MockCGMDataSource.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. //
  2. // MockCGMDataSource.swift
  3. // LoopKit
  4. //
  5. // Created by Michael Pangburn on 11/23/18.
  6. // Copyright © 2018 LoopKit Authors. All rights reserved.
  7. //
  8. import HealthKit
  9. import LoopKit
  10. public struct MockCGMDataSource {
  11. public enum Model {
  12. public typealias SineCurveParameters = (baseGlucose: HKQuantity, amplitude: HKQuantity, period: TimeInterval, referenceDate: Date)
  13. case constant(_ glucose: HKQuantity)
  14. case sineCurve(parameters: SineCurveParameters)
  15. case noData
  16. }
  17. public struct Effects {
  18. public typealias RandomOutlier = (chance: Double, delta: HKQuantity)
  19. public var glucoseNoise: HKQuantity?
  20. public var randomLowOutlier: RandomOutlier?
  21. public var randomHighOutlier: RandomOutlier?
  22. public var randomErrorChance: Double?
  23. public init(
  24. glucoseNoise: HKQuantity? = nil,
  25. randomLowOutlier: RandomOutlier? = nil,
  26. randomHighOutlier: RandomOutlier? = nil,
  27. randomErrorChance: Double? = nil
  28. ) {
  29. self.glucoseNoise = glucoseNoise
  30. self.randomLowOutlier = randomLowOutlier
  31. self.randomHighOutlier = randomHighOutlier
  32. self.randomErrorChance = randomErrorChance
  33. }
  34. }
  35. static let device = HKDevice(
  36. name: MockCGMManager.managerIdentifier,
  37. manufacturer: nil,
  38. model: nil,
  39. hardwareVersion: nil,
  40. firmwareVersion: nil,
  41. softwareVersion: String(LoopKitVersionNumber),
  42. localIdentifier: nil,
  43. udiDeviceIdentifier: nil
  44. )
  45. public var model: Model {
  46. didSet {
  47. glucoseProvider = MockGlucoseProvider(model: model, effects: effects)
  48. }
  49. }
  50. public var effects: Effects {
  51. didSet {
  52. glucoseProvider = MockGlucoseProvider(model: model, effects: effects)
  53. }
  54. }
  55. private var glucoseProvider: MockGlucoseProvider
  56. private var lastFetchedData = Locked(Date.distantPast)
  57. let dataPointFrequency: TimeInterval
  58. public init(
  59. model: Model,
  60. effects: Effects = .init(),
  61. dataPointFrequency: TimeInterval = /* minutes */ 5 * 60
  62. ) {
  63. self.model = model
  64. self.effects = effects
  65. self.glucoseProvider = MockGlucoseProvider(model: model, effects: effects)
  66. self.dataPointFrequency = dataPointFrequency
  67. }
  68. func fetchNewData(_ completion: @escaping (CGMResult) -> Void) {
  69. let now = Date()
  70. // Give 5% wiggle room for producing data points
  71. let bufferedFrequency = dataPointFrequency - 0.05 * dataPointFrequency
  72. if now.timeIntervalSince(lastFetchedData.value) < bufferedFrequency {
  73. completion(.noData)
  74. return
  75. }
  76. lastFetchedData.value = now
  77. glucoseProvider.fetchData(at: now, completion: completion)
  78. }
  79. func backfillData(from interval: DateInterval, completion: @escaping (CGMResult) -> Void) {
  80. lastFetchedData.value = interval.end
  81. let request = MockGlucoseProvider.BackfillRequest(datingBack: interval.duration, dataPointFrequency: dataPointFrequency)
  82. glucoseProvider.backfill(request, endingAt: interval.end, completion: completion)
  83. }
  84. }
  85. extension MockCGMDataSource: RawRepresentable {
  86. public typealias RawValue = [String: Any]
  87. public init?(rawValue: RawValue) {
  88. guard
  89. let model = (rawValue["model"] as? Model.RawValue).flatMap(Model.init(rawValue:)),
  90. let effects = (rawValue["effects"] as? Effects.RawValue).flatMap(Effects.init(rawValue:))
  91. else {
  92. return nil
  93. }
  94. self.init(model: model, effects: effects)
  95. }
  96. public var rawValue: RawValue {
  97. return [
  98. "model": model.rawValue,
  99. "effects": effects.rawValue
  100. ]
  101. }
  102. }
  103. extension MockCGMDataSource.Model: RawRepresentable {
  104. public typealias RawValue = [String: Any]
  105. private enum Kind: String {
  106. case constant = "constant"
  107. case sineCurve = "sineCurve"
  108. case noData = "noData"
  109. }
  110. private static let unit = HKUnit.milligramsPerDeciliter
  111. public init?(rawValue: RawValue) {
  112. guard
  113. let kindRawValue = rawValue["kind"] as? Kind.RawValue,
  114. let kind = Kind(rawValue: kindRawValue)
  115. else {
  116. return nil
  117. }
  118. let unit = MockCGMDataSource.Model.unit
  119. func glucose(forKey key: String) -> HKQuantity? {
  120. guard let doubleValue = rawValue[key] as? Double else {
  121. return nil
  122. }
  123. return HKQuantity(unit: unit, doubleValue: doubleValue)
  124. }
  125. switch kind {
  126. case .constant:
  127. guard let quantity = glucose(forKey: "quantity") else {
  128. return nil
  129. }
  130. self = .constant(quantity)
  131. case .sineCurve:
  132. guard
  133. let baseGlucose = glucose(forKey: "baseGlucose"),
  134. let amplitude = glucose(forKey: "amplitude"),
  135. let period = rawValue["period"] as? TimeInterval,
  136. let referenceDateSeconds = rawValue["referenceDate"] as? TimeInterval
  137. else {
  138. return nil
  139. }
  140. let referenceDate = Date(timeIntervalSince1970: referenceDateSeconds)
  141. self = .sineCurve(parameters: (baseGlucose: baseGlucose, amplitude: amplitude, period: period, referenceDate: referenceDate))
  142. case .noData:
  143. self = .noData
  144. }
  145. }
  146. public var rawValue: RawValue {
  147. var rawValue: RawValue = ["kind": kind.rawValue]
  148. let unit = MockCGMDataSource.Model.unit
  149. switch self {
  150. case .constant(let quantity):
  151. rawValue["quantity"] = quantity.doubleValue(for: unit)
  152. case .sineCurve(parameters: (baseGlucose: let baseGlucose, amplitude: let amplitude, period: let period, referenceDate: let referenceDate)):
  153. rawValue["baseGlucose"] = baseGlucose.doubleValue(for: unit)
  154. rawValue["amplitude"] = amplitude.doubleValue(for: unit)
  155. rawValue["period"] = period
  156. rawValue["referenceDate"] = referenceDate.timeIntervalSince1970
  157. case .noData:
  158. break
  159. }
  160. return rawValue
  161. }
  162. private var kind: Kind {
  163. switch self {
  164. case .constant:
  165. return .constant
  166. case .sineCurve:
  167. return .sineCurve
  168. case .noData:
  169. return .noData
  170. }
  171. }
  172. }
  173. extension MockCGMDataSource.Effects: RawRepresentable {
  174. public typealias RawValue = [String: Any]
  175. private static let unit = HKUnit.milligramsPerDeciliter
  176. public init?(rawValue: RawValue) {
  177. self.init()
  178. let unit = MockCGMDataSource.Effects.unit
  179. func randomOutlier(forKey key: String) -> RandomOutlier? {
  180. guard
  181. let outlier = rawValue[key] as? [String: Double],
  182. let chance = outlier["chance"],
  183. let delta = outlier["delta"]
  184. else {
  185. return nil
  186. }
  187. return (chance: chance, delta: HKQuantity(unit: unit, doubleValue: delta))
  188. }
  189. if let glucoseNoise = rawValue["glucoseNoise"] as? Double {
  190. self.glucoseNoise = HKQuantity(unit: unit, doubleValue: glucoseNoise)
  191. }
  192. self.randomLowOutlier = randomOutlier(forKey: "randomLowOutlier")
  193. self.randomHighOutlier = randomOutlier(forKey: "randomHighOutlier")
  194. self.randomErrorChance = rawValue["randomErrorChance"] as? Double
  195. }
  196. public var rawValue: RawValue {
  197. var rawValue: RawValue = [:]
  198. let unit = MockCGMDataSource.Effects.unit
  199. func insertOutlier(_ outlier: RandomOutlier, forKey key: String) {
  200. rawValue[key] = [
  201. "chance": outlier.chance,
  202. "delta": outlier.delta.doubleValue(for: unit)
  203. ]
  204. }
  205. if let glucoseNoise = glucoseNoise {
  206. rawValue["glucoseNoise"] = glucoseNoise.doubleValue(for: unit)
  207. }
  208. if let randomLowOutlier = randomLowOutlier {
  209. insertOutlier(randomLowOutlier, forKey: "randomLowOutlier")
  210. }
  211. if let randomHighOutlier = randomHighOutlier {
  212. insertOutlier(randomHighOutlier, forKey: "randomHighOutlier")
  213. }
  214. if let randomErrorChance = randomErrorChance {
  215. rawValue["randomErrorChance"] = randomErrorChance
  216. }
  217. return rawValue
  218. }
  219. }
  220. extension MockCGMDataSource: CustomDebugStringConvertible {
  221. public var debugDescription: String {
  222. return """
  223. ## MockCGMDataSource
  224. * model: \(model)
  225. * effects: \(effects)
  226. """
  227. }
  228. }