MinimedPumpManagerState.swift 15 KB

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