MockCGMDataSource.swift 10 KB

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