MockCGMDataSource.swift 11 KB

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