MinimedPumpManagerState.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. //
  2. // MinimedPumpManagerState.swift
  3. // Loop
  4. //
  5. // Copyright © 2018 LoopKit Authors. All rights reserved.
  6. //
  7. import LoopKit
  8. import RileyLinkKit
  9. import RileyLinkBLEKit
  10. public struct ReconciledDoseMapping: Equatable {
  11. let startTime: Date
  12. let uuid: UUID
  13. let eventRaw: Data
  14. }
  15. extension ReconciledDoseMapping: RawRepresentable {
  16. public typealias RawValue = [String:Any]
  17. public init?(rawValue: [String : Any]) {
  18. guard
  19. let startTime = rawValue["startTime"] as? Date,
  20. let uuidString = rawValue["uuid"] as? String,
  21. let uuid = UUID(uuidString: uuidString),
  22. let eventRawString = rawValue["eventRaw"] as? String,
  23. let eventRaw = Data(hexadecimalString: eventRawString) else
  24. {
  25. return nil
  26. }
  27. self.startTime = startTime
  28. self.uuid = uuid
  29. self.eventRaw = eventRaw
  30. }
  31. public var rawValue: [String : Any] {
  32. return [
  33. "startTime": startTime,
  34. "uuid": uuid.uuidString,
  35. "eventRaw": eventRaw.hexadecimalString,
  36. ]
  37. }
  38. }
  39. public struct MinimedPumpManagerState: RawRepresentable, Equatable {
  40. public typealias RawValue = PumpManager.RawStateValue
  41. public static let version = 2
  42. public var isOnboarded: Bool
  43. public var batteryChemistry: BatteryChemistryType = .alkaline
  44. public var batteryPercentage: Double?
  45. public var suspendState: SuspendState
  46. public var lastReservoirReading: ReservoirReading?
  47. public var lastTuned: Date? // In-memory only
  48. public var lastValidFrequency: Measurement<UnitFrequency>?
  49. public var preferredInsulinDataSource: InsulinDataSource = .pumpHistory
  50. public var useMySentry: Bool
  51. public let pumpColor: PumpColor
  52. public let pumpModel: PumpModel
  53. public let pumpFirmwareVersion: String
  54. public let pumpID: String
  55. public let pumpRegion: PumpRegion
  56. public var pumpSettings: PumpSettings {
  57. get {
  58. return PumpSettings(pumpID: pumpID, pumpRegion: pumpRegion)
  59. }
  60. }
  61. public var pumpState: PumpState {
  62. get {
  63. var state = PumpState()
  64. state.pumpModel = pumpModel
  65. state.timeZone = timeZone
  66. state.lastValidFrequency = lastValidFrequency
  67. state.lastTuned = lastTuned
  68. state.useMySentry = useMySentry
  69. return state
  70. }
  71. set {
  72. lastValidFrequency = newValue.lastValidFrequency
  73. lastTuned = newValue.lastTuned
  74. timeZone = newValue.timeZone
  75. }
  76. }
  77. public var rileyLinkConnectionState: RileyLinkConnectionState?
  78. public var timeZone: TimeZone
  79. public var unfinalizedBolus: UnfinalizedDose?
  80. public var unfinalizedTempBasal: UnfinalizedDose?
  81. // Doses we're tracking that haven't shown up in history yet
  82. public var pendingDoses: [UnfinalizedDose] = []
  83. // Maps
  84. public var reconciliationMappings: [Data:ReconciledDoseMapping] = [:]
  85. public var lastReconciliation: Date?
  86. public var insulinType: InsulinType?
  87. public var rileyLinkBatteryAlertLevel: Int?
  88. public var lastRileyLinkBatteryAlertDate: Date = .distantPast
  89. public init(isOnboarded: Bool, useMySentry: Bool, pumpColor: PumpColor, pumpID: String, pumpModel: PumpModel, pumpFirmwareVersion: String, pumpRegion: PumpRegion, rileyLinkConnectionState: RileyLinkConnectionState?, timeZone: TimeZone, suspendState: SuspendState, insulinType: InsulinType, lastTuned: Date?, lastValidFrequency: Measurement<UnitFrequency>?)
  90. {
  91. self.isOnboarded = isOnboarded
  92. self.useMySentry = useMySentry
  93. self.pumpColor = pumpColor
  94. self.pumpID = pumpID
  95. self.pumpModel = pumpModel
  96. self.pumpFirmwareVersion = pumpFirmwareVersion
  97. self.pumpRegion = pumpRegion
  98. self.rileyLinkConnectionState = rileyLinkConnectionState
  99. self.timeZone = timeZone
  100. self.suspendState = suspendState
  101. self.insulinType = insulinType
  102. self.lastTuned = lastTuned
  103. self.lastValidFrequency = lastValidFrequency
  104. }
  105. public init?(rawValue: RawValue) {
  106. guard
  107. let version = rawValue["version"] as? Int,
  108. let useMySentry = rawValue["useMySentry"] as? Bool,
  109. let pumpID = rawValue["pumpID"] as? String,
  110. let batteryChemistryRaw = rawValue["batteryChemistry"] as? BatteryChemistryType.RawValue,
  111. let batteryChemistry = BatteryChemistryType(rawValue: batteryChemistryRaw),
  112. let insulinDataSourceRaw = rawValue["insulinDataSource"] as? InsulinDataSource.RawValue,
  113. let insulinDataSource = InsulinDataSource(rawValue: insulinDataSourceRaw),
  114. let pumpColorRaw = rawValue["pumpColor"] as? PumpColor.RawValue,
  115. let pumpColor = PumpColor(rawValue: pumpColorRaw),
  116. let pumpModelNumber = rawValue["pumpModel"] as? PumpModel.RawValue,
  117. let pumpModel = PumpModel(rawValue: pumpModelNumber),
  118. let pumpRegionRaw = rawValue["pumpRegion"] as? PumpRegion.RawValue,
  119. let pumpRegion = PumpRegion(rawValue: pumpRegionRaw),
  120. let timeZoneSeconds = rawValue["timeZone"] as? Int,
  121. let timeZone = TimeZone(secondsFromGMT: timeZoneSeconds)
  122. else {
  123. return nil
  124. }
  125. // Migrate
  126. if version == 1
  127. {
  128. if let oldRileyLinkPumpManagerStateRaw = rawValue["rileyLinkPumpManagerState"] as? [String : Any],
  129. let connectedPeripheralIDs = oldRileyLinkPumpManagerStateRaw["connectedPeripheralIDs"] as? [String]
  130. {
  131. self.rileyLinkConnectionState = RileyLinkConnectionState(autoConnectIDs: Set(connectedPeripheralIDs))
  132. }
  133. } else {
  134. if let rawState = rawValue["rileyLinkConnectionManagerState"] as? RileyLinkConnectionState.RawValue {
  135. self.rileyLinkConnectionState = RileyLinkConnectionState(rawValue: rawState)
  136. }
  137. }
  138. self.useMySentry = useMySentry
  139. self.pumpID = pumpID
  140. self.batteryChemistry = batteryChemistry
  141. self.preferredInsulinDataSource = insulinDataSource
  142. self.pumpColor = pumpColor
  143. self.pumpModel = pumpModel
  144. self.pumpRegion = pumpRegion
  145. self.timeZone = timeZone
  146. isOnboarded = rawValue["isOnboarded"] as? Bool ?? true // Backward compatibility
  147. if let isPumpSuspended = rawValue["isPumpSuspended"] as? Bool {
  148. // migrate
  149. if isPumpSuspended {
  150. suspendState = .suspended(Date())
  151. } else {
  152. suspendState = .resumed(Date())
  153. }
  154. } else if let rawSuspendState = rawValue["suspendState"] as? SuspendState.RawValue, let storedSuspendState = SuspendState(rawValue: rawSuspendState) {
  155. suspendState = storedSuspendState
  156. } else {
  157. return nil
  158. }
  159. if let frequencyRaw = rawValue["lastValidFrequency"] as? Double {
  160. lastValidFrequency = Measurement<UnitFrequency>(value: frequencyRaw, unit: .megahertz)
  161. } else {
  162. lastValidFrequency = nil
  163. }
  164. pumpFirmwareVersion = (rawValue["pumpFirmwareVersion"] as? String) ?? ""
  165. batteryPercentage = rawValue["batteryPercentage"] as? Double
  166. if let rawLastReservoirReading = rawValue["lastReservoirReading"] as? ReservoirReading.RawValue {
  167. lastReservoirReading = ReservoirReading(rawValue: rawLastReservoirReading)
  168. }
  169. if let rawUnfinalizedBolus = rawValue["unfinalizedBolus"] as? UnfinalizedDose.RawValue {
  170. unfinalizedBolus = UnfinalizedDose(rawValue: rawUnfinalizedBolus)
  171. }
  172. if let rawUnfinalizedTempBasal = rawValue["unfinalizedTempBasal"] as? UnfinalizedDose.RawValue {
  173. unfinalizedTempBasal = UnfinalizedDose(rawValue: rawUnfinalizedTempBasal)
  174. }
  175. if let rawPendingDoses = rawValue["pendingDoses"] as? [UnfinalizedDose.RawValue] {
  176. pendingDoses = rawPendingDoses.compactMap( { UnfinalizedDose(rawValue: $0) } )
  177. } else {
  178. pendingDoses = []
  179. }
  180. let recentlyReconciledEvents: [Data:ReconciledDoseMapping]
  181. if let rawRecentlyReconciledEvents = rawValue["recentlyReconciledEvents"] as? [ReconciledDoseMapping.RawValue] {
  182. let mappings = rawRecentlyReconciledEvents.compactMap { ReconciledDoseMapping(rawValue: $0) }
  183. recentlyReconciledEvents = Dictionary(mappings.map{ ($0.eventRaw, $0) }, uniquingKeysWith: { (old, new) in new } )
  184. } else {
  185. recentlyReconciledEvents = [:]
  186. }
  187. reconciliationMappings = recentlyReconciledEvents
  188. lastReconciliation = rawValue["lastReconciliation"] as? Date
  189. if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue {
  190. insulinType = InsulinType(rawValue: rawInsulinType)
  191. }
  192. rileyLinkBatteryAlertLevel = rawValue["rileyLinkBatteryAlertLevel"] as? Int
  193. lastRileyLinkBatteryAlertDate = rawValue["lastRileyLinkBatteryAlertDate"] as? Date ?? Date.distantPast
  194. }
  195. public var rawValue: RawValue {
  196. var value: [String : Any] = [
  197. "isOnboarded": isOnboarded,
  198. "batteryChemistry": batteryChemistry.rawValue,
  199. "insulinDataSource": preferredInsulinDataSource.rawValue,
  200. "pumpColor": pumpColor.rawValue,
  201. "pumpID": pumpID,
  202. "pumpModel": pumpModel.rawValue,
  203. "pumpFirmwareVersion": pumpFirmwareVersion,
  204. "pumpRegion": pumpRegion.rawValue,
  205. "timeZone": timeZone.secondsFromGMT(),
  206. "suspendState": suspendState.rawValue,
  207. "version": MinimedPumpManagerState.version,
  208. "pendingDoses": pendingDoses.map { $0.rawValue },
  209. "recentlyReconciledEvents": reconciliationMappings.values.map { $0.rawValue },
  210. ]
  211. value["useMySentry"] = useMySentry
  212. value["batteryPercentage"] = batteryPercentage
  213. value["lastReservoirReading"] = lastReservoirReading?.rawValue
  214. value["lastValidFrequency"] = lastValidFrequency?.converted(to: .megahertz).value
  215. value["rileyLinkConnectionManagerState"] = rileyLinkConnectionState?.rawValue
  216. value["unfinalizedBolus"] = unfinalizedBolus?.rawValue
  217. value["unfinalizedTempBasal"] = unfinalizedTempBasal?.rawValue
  218. value["lastReconciliation"] = lastReconciliation
  219. value["insulinType"] = insulinType?.rawValue
  220. value["rileyLinkBatteryAlertLevel"] = rileyLinkBatteryAlertLevel
  221. value["lastRileyLinkBatteryAlertDate"] = lastRileyLinkBatteryAlertDate
  222. return value
  223. }
  224. }
  225. extension MinimedPumpManagerState {
  226. static let idleListeningEnabledDefaults: RileyLinkBluetoothDevice.IdleListeningState = .enabled(timeout: .minutes(4), channel: 0)
  227. }
  228. extension MinimedPumpManagerState: CustomDebugStringConvertible {
  229. public var debugDescription: String {
  230. return [
  231. "## MinimedPumpManagerState",
  232. "isOnboarded: \(isOnboarded)",
  233. "batteryChemistry: \(batteryChemistry)",
  234. "batteryPercentage: \(String(describing: batteryPercentage))",
  235. "suspendState: \(suspendState)",
  236. "lastValidFrequency: \(String(describing: lastValidFrequency))",
  237. "preferredInsulinDataSource: \(preferredInsulinDataSource)",
  238. "useMySentry: \(useMySentry)",
  239. "pumpColor: \(pumpColor)",
  240. "pumpID: ✔︎",
  241. "pumpModel: \(pumpModel.rawValue)",
  242. "pumpFirmwareVersion: \(pumpFirmwareVersion)",
  243. "pumpRegion: \(pumpRegion)",
  244. "reservoirUnits: \(String(describing: lastReservoirReading?.units))",
  245. "reservoirValidAt: \(String(describing: lastReservoirReading?.validAt))",
  246. "unfinalizedBolus: \(String(describing: unfinalizedBolus))",
  247. "unfinalizedTempBasal: \(String(describing: unfinalizedTempBasal))",
  248. "pendingDoses: \(pendingDoses)",
  249. "timeZone: \(timeZone)",
  250. "recentlyReconciledEvents: \(reconciliationMappings.values.map { "\($0.eventRaw.hexadecimalString) -> \($0.uuid)" })",
  251. "lastReconciliation: \(String(describing: lastReconciliation))",
  252. "insulinType: \(String(describing: insulinType))",
  253. "rileyLinkBatteryAlertLevel: \(String(describing: rileyLinkBatteryAlertLevel))",
  254. "lastRileyLinkBatteryAlertDate \(String(describing: lastRileyLinkBatteryAlertDate))",
  255. String(reflecting: rileyLinkConnectionState),
  256. ].joined(separator: "\n")
  257. }
  258. }
  259. public enum SuspendState: Equatable, RawRepresentable {
  260. public typealias RawValue = [String: Any]
  261. private enum SuspendStateType: Int {
  262. case suspend, resume
  263. }
  264. case suspended(Date)
  265. case resumed(Date)
  266. private var identifier: Int {
  267. switch self {
  268. case .suspended:
  269. return 1
  270. case .resumed:
  271. return 2
  272. }
  273. }
  274. public init?(rawValue: RawValue) {
  275. guard let suspendStateType = rawValue["case"] as? SuspendStateType.RawValue,
  276. let date = rawValue["date"] as? Date else {
  277. return nil
  278. }
  279. switch SuspendStateType(rawValue: suspendStateType) {
  280. case .suspend?:
  281. self = .suspended(date)
  282. case .resume?:
  283. self = .resumed(date)
  284. default:
  285. return nil
  286. }
  287. }
  288. public var rawValue: RawValue {
  289. switch self {
  290. case .suspended(let date):
  291. return [
  292. "case": SuspendStateType.suspend.rawValue,
  293. "date": date
  294. ]
  295. case .resumed(let date):
  296. return [
  297. "case": SuspendStateType.resume.rawValue,
  298. "date": date
  299. ]
  300. }
  301. }
  302. }