MockCGMDataSource.swift 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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. case signalLoss
  17. public var isValidSession: Bool {
  18. switch self {
  19. case .noData:
  20. return false
  21. default:
  22. return true
  23. }
  24. }
  25. }
  26. public struct Effects {
  27. public typealias RandomOutlier = (chance: Double, delta: HKQuantity)
  28. public var glucoseNoise: HKQuantity?
  29. public var randomLowOutlier: RandomOutlier?
  30. public var randomHighOutlier: RandomOutlier?
  31. public var randomErrorChance: Double?
  32. public init(
  33. glucoseNoise: HKQuantity? = nil,
  34. randomLowOutlier: RandomOutlier? = nil,
  35. randomHighOutlier: RandomOutlier? = nil,
  36. randomErrorChance: Double? = nil
  37. ) {
  38. self.glucoseNoise = glucoseNoise
  39. self.randomLowOutlier = randomLowOutlier
  40. self.randomHighOutlier = randomHighOutlier
  41. self.randomErrorChance = randomErrorChance
  42. }
  43. }
  44. static let device = HKDevice(
  45. name: MockCGMManager.managerIdentifier,
  46. manufacturer: nil,
  47. model: nil,
  48. hardwareVersion: nil,
  49. firmwareVersion: nil,
  50. softwareVersion: String(LoopKitVersionNumber),
  51. localIdentifier: nil,
  52. udiDeviceIdentifier: nil
  53. )
  54. public var model: Model {
  55. didSet {
  56. glucoseProvider = MockGlucoseProvider(model: model, effects: effects)
  57. }
  58. }
  59. public var effects: Effects {
  60. didSet {
  61. glucoseProvider = MockGlucoseProvider(model: model, effects: effects)
  62. }
  63. }
  64. private var glucoseProvider: MockGlucoseProvider
  65. private var lastFetchedData = Locked(Date.distantPast)
  66. public var dataPointFrequency: MeasurementFrequency
  67. public var isValidSession: Bool {
  68. return model.isValidSession
  69. }
  70. public init(
  71. model: Model,
  72. effects: Effects = .init(),
  73. dataPointFrequency: MeasurementFrequency = .normal
  74. ) {
  75. self.model = model
  76. self.effects = effects
  77. self.glucoseProvider = MockGlucoseProvider(model: model, effects: effects)
  78. self.dataPointFrequency = dataPointFrequency
  79. }
  80. func fetchNewData(_ completion: @escaping (CGMReadingResult) -> Void) {
  81. let now = Date()
  82. // Give 5% wiggle room for producing data points
  83. let bufferedFrequency = dataPointFrequency.frequency - 0.05 * dataPointFrequency.frequency
  84. if now.timeIntervalSince(lastFetchedData.value) < bufferedFrequency {
  85. completion(.noData)
  86. return
  87. }
  88. lastFetchedData.value = now
  89. glucoseProvider.fetchData(at: now, completion: completion)
  90. }
  91. func backfillData(from interval: DateInterval, completion: @escaping (CGMReadingResult) -> Void) {
  92. lastFetchedData.value = interval.end
  93. let request = MockGlucoseProvider.BackfillRequest(datingBack: interval.duration, dataPointFrequency: dataPointFrequency.frequency)
  94. glucoseProvider.backfill(request, endingAt: interval.end, completion: completion)
  95. }
  96. }
  97. extension MockCGMDataSource: RawRepresentable {
  98. public typealias RawValue = [String: Any]
  99. public init?(rawValue: RawValue) {
  100. guard
  101. let model = (rawValue["model"] as? Model.RawValue).flatMap(Model.init(rawValue:)),
  102. let effects = (rawValue["effects"] as? Effects.RawValue).flatMap(Effects.init(rawValue:)),
  103. let dataPointFrequency = (rawValue["dataPointFrequency"] as? MeasurementFrequency.RawValue).flatMap(MeasurementFrequency.init(rawValue:))
  104. else {
  105. return nil
  106. }
  107. self.init(model: model, effects: effects, dataPointFrequency: dataPointFrequency)
  108. }
  109. public var rawValue: RawValue {
  110. return [
  111. "model": model.rawValue,
  112. "effects": effects.rawValue,
  113. "dataPointFrequency": dataPointFrequency.rawValue
  114. ]
  115. }
  116. }
  117. extension MockCGMDataSource.Model: RawRepresentable {
  118. public typealias RawValue = [String: Any]
  119. private enum Kind: String {
  120. case constant = "constant"
  121. case sineCurve = "sineCurve"
  122. case noData = "noData"
  123. case signalLoss = "signalLoss"
  124. }
  125. private static let unit = HKUnit.milligramsPerDeciliter
  126. public init?(rawValue: RawValue) {
  127. guard
  128. let kindRawValue = rawValue["kind"] as? Kind.RawValue,
  129. let kind = Kind(rawValue: kindRawValue)
  130. else {
  131. return nil
  132. }
  133. let unit = MockCGMDataSource.Model.unit
  134. func glucose(forKey key: String) -> HKQuantity? {
  135. guard let doubleValue = rawValue[key] as? Double else {
  136. return nil
  137. }
  138. return HKQuantity(unit: unit, doubleValue: doubleValue)
  139. }
  140. switch kind {
  141. case .constant:
  142. guard let quantity = glucose(forKey: "quantity") else {
  143. return nil
  144. }
  145. self = .constant(quantity)
  146. case .sineCurve:
  147. guard
  148. let baseGlucose = glucose(forKey: "baseGlucose"),
  149. let amplitude = glucose(forKey: "amplitude"),
  150. let period = rawValue["period"] as? TimeInterval,
  151. let referenceDateSeconds = rawValue["referenceDate"] as? TimeInterval
  152. else {
  153. return nil
  154. }
  155. let referenceDate = Date(timeIntervalSince1970: referenceDateSeconds)
  156. self = .sineCurve(parameters: (baseGlucose: baseGlucose, amplitude: amplitude, period: period, referenceDate: referenceDate))
  157. case .noData:
  158. self = .noData
  159. case .signalLoss:
  160. self = .signalLoss
  161. }
  162. }
  163. public var rawValue: RawValue {
  164. var rawValue: RawValue = ["kind": kind.rawValue]
  165. let unit = MockCGMDataSource.Model.unit
  166. switch self {
  167. case .constant(let quantity):
  168. rawValue["quantity"] = quantity.doubleValue(for: unit)
  169. case .sineCurve(parameters: (baseGlucose: let baseGlucose, amplitude: let amplitude, period: let period, referenceDate: let referenceDate)):
  170. rawValue["baseGlucose"] = baseGlucose.doubleValue(for: unit)
  171. rawValue["amplitude"] = amplitude.doubleValue(for: unit)
  172. rawValue["period"] = period
  173. rawValue["referenceDate"] = referenceDate.timeIntervalSince1970
  174. case .noData, .signalLoss:
  175. break
  176. }
  177. return rawValue
  178. }
  179. private var kind: Kind {
  180. switch self {
  181. case .constant:
  182. return .constant
  183. case .sineCurve:
  184. return .sineCurve
  185. case .noData:
  186. return .noData
  187. case .signalLoss:
  188. return .signalLoss
  189. }
  190. }
  191. }
  192. extension MockCGMDataSource.Effects: RawRepresentable {
  193. public typealias RawValue = [String: Any]
  194. private static let unit = HKUnit.milligramsPerDeciliter
  195. public init?(rawValue: RawValue) {
  196. self.init()
  197. let unit = MockCGMDataSource.Effects.unit
  198. func randomOutlier(forKey key: String) -> RandomOutlier? {
  199. guard
  200. let outlier = rawValue[key] as? [String: Double],
  201. let chance = outlier["chance"],
  202. let delta = outlier["delta"]
  203. else {
  204. return nil
  205. }
  206. return (chance: chance, delta: HKQuantity(unit: unit, doubleValue: delta))
  207. }
  208. if let glucoseNoise = rawValue["glucoseNoise"] as? Double {
  209. self.glucoseNoise = HKQuantity(unit: unit, doubleValue: glucoseNoise)
  210. }
  211. self.randomLowOutlier = randomOutlier(forKey: "randomLowOutlier")
  212. self.randomHighOutlier = randomOutlier(forKey: "randomHighOutlier")
  213. self.randomErrorChance = rawValue["randomErrorChance"] as? Double
  214. }
  215. public var rawValue: RawValue {
  216. var rawValue: RawValue = [:]
  217. let unit = MockCGMDataSource.Effects.unit
  218. func insertOutlier(_ outlier: RandomOutlier, forKey key: String) {
  219. rawValue[key] = [
  220. "chance": outlier.chance,
  221. "delta": outlier.delta.doubleValue(for: unit)
  222. ]
  223. }
  224. if let glucoseNoise = glucoseNoise {
  225. rawValue["glucoseNoise"] = glucoseNoise.doubleValue(for: unit)
  226. }
  227. if let randomLowOutlier = randomLowOutlier {
  228. insertOutlier(randomLowOutlier, forKey: "randomLowOutlier")
  229. }
  230. if let randomHighOutlier = randomHighOutlier {
  231. insertOutlier(randomHighOutlier, forKey: "randomHighOutlier")
  232. }
  233. if let randomErrorChance = randomErrorChance {
  234. rawValue["randomErrorChance"] = randomErrorChance
  235. }
  236. return rawValue
  237. }
  238. }
  239. extension MockCGMDataSource: CustomDebugStringConvertible {
  240. public var debugDescription: String {
  241. return """
  242. ## MockCGMDataSource
  243. * model: \(model)
  244. * effects: \(effects)
  245. """
  246. }
  247. }
  248. public enum MeasurementFrequency: Int, CaseIterable {
  249. case fast
  250. case normal
  251. public var frequency: TimeInterval {
  252. switch self {
  253. case .fast:
  254. return TimeInterval(5)
  255. case .normal:
  256. return TimeInterval(5*60)
  257. }
  258. }
  259. public var localizedDescription: String {
  260. switch self {
  261. case .fast:
  262. return "5 seconds"
  263. case .normal:
  264. return "5 minutes"
  265. }
  266. }
  267. }