WatchState.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import Foundation
  2. import WatchConnectivity
  3. /// WatchState manages the communication between the Watch app and the iPhone app using WatchConnectivity.
  4. /// It handles glucose data synchronization and sending treatment requests (bolus, carbs) to the phone.
  5. @Observable final class WatchState: NSObject, WCSessionDelegate {
  6. // MARK: - Properties
  7. /// The WatchConnectivity session instance used for communication
  8. private var session: WCSession?
  9. /// Indicates if the paired iPhone is currently reachable
  10. var isReachable = false
  11. var currentGlucose: String = "--"
  12. var trend: String? = ""
  13. var delta: String? = "--"
  14. var glucoseValues: [(date: Date, glucose: Double)] = []
  15. var cob: String? = "--"
  16. var iob: String? = "--"
  17. var lastLoopTime: String? = "--"
  18. var overridePresets: [OverridePresetWatch] = []
  19. var tempTargetPresets: [TempTargetPresetWatch] = []
  20. /// treatments inputs
  21. /// used to store carbs for combined meal-bolus-treatments
  22. var carbsAmount: Int = 0
  23. var fatAmount: Int = 0
  24. var proteinAmount: Int = 0
  25. var bolusAmount = 0.0
  26. var confirmationProgress = 0.0
  27. override init() {
  28. super.init()
  29. setupSession()
  30. }
  31. /// Configures the WatchConnectivity session if supported on the device
  32. private func setupSession() {
  33. if WCSession.isSupported() {
  34. let session = WCSession.default
  35. session.delegate = self
  36. session.activate()
  37. self.session = session
  38. } else {
  39. print("⌚️ WCSession is not supported on this device")
  40. }
  41. }
  42. // MARK: - Send Data to Phone
  43. /// Sends a bolus insulin request to the paired iPhone
  44. /// - Parameters:
  45. /// - amount: The insulin amount to be delivered
  46. func sendBolusRequest(_ amount: Decimal) {
  47. guard let session = session, session.isReachable else { return }
  48. let message: [String: Any] = [
  49. "bolus": amount
  50. ]
  51. session.sendMessage(message, replyHandler: nil) { error in
  52. print("Error sending bolus request: \(error.localizedDescription)")
  53. }
  54. }
  55. /// Sends a carbohydrate entry request to the paired iPhone
  56. /// - Parameters:
  57. /// - amount: The amount of carbs in grams
  58. /// - date: The timestamp for the carb entry (defaults to current time)
  59. func sendCarbsRequest(_ amount: Int, _ date: Date = Date()) {
  60. guard let session = session, session.isReachable else { return }
  61. let message: [String: Any] = [
  62. "carbs": amount,
  63. "date": date.timeIntervalSince1970
  64. ]
  65. session.sendMessage(message, replyHandler: nil) { error in
  66. print("Error sending carbs request: \(error.localizedDescription)")
  67. }
  68. }
  69. /// Sends a meal and bolus insulin combo request to the paired iPhone
  70. /// - Parameters:
  71. /// - amount: The insulin amount to be delivered
  72. /// - isExternal: Indicates if the bolus is from an external source
  73. func sendMealBolusComboRequest(carbsAmount _: Decimal, bolusAmount: Decimal, _ date: Date = Date()) {
  74. guard let session = session, session.isReachable else { return }
  75. let message: [String: Any] = [
  76. "bolus": bolusAmount,
  77. "carbs": bolusAmount,
  78. "date": date.timeIntervalSince1970
  79. ]
  80. session.sendMessage(message, replyHandler: nil) { error in
  81. print("Error sending meal bolus combo request: \(error.localizedDescription)")
  82. }
  83. }
  84. func sendCancelOverrideRequest() {
  85. guard let session = session, session.isReachable else { return }
  86. let message: [String: Any] = [
  87. "cancelOverride": true
  88. ]
  89. session.sendMessage(message, replyHandler: nil) { error in
  90. print("⌚️ Error sending cancel override request: \(error.localizedDescription)")
  91. }
  92. }
  93. func sendActivateOverrideRequest(presetName: String) {
  94. guard let session = session, session.isReachable else { return }
  95. let message: [String: Any] = [
  96. "activateOverride": presetName
  97. ]
  98. session.sendMessage(message, replyHandler: nil) { error in
  99. print("⌚️ Error sending activate override request: \(error.localizedDescription)")
  100. }
  101. }
  102. func sendCancelTempTargetRequest() {
  103. guard let session = session, session.isReachable else { return }
  104. let message: [String: Any] = [
  105. "cancelTempTarget": true
  106. ]
  107. session.sendMessage(message, replyHandler: nil) { error in
  108. print("⌚️ Error sending cancel temp target request: \(error.localizedDescription)")
  109. }
  110. }
  111. func sendActivateTempTargetRequest(presetName: String) {
  112. guard let session = session, session.isReachable else { return }
  113. let message: [String: Any] = [
  114. "activateTempTarget": presetName
  115. ]
  116. session.sendMessage(message, replyHandler: nil) { error in
  117. print("⌚️ Error sending activate temp target request: \(error.localizedDescription)")
  118. }
  119. }
  120. // MARK: - WCSessionDelegate
  121. /// Called when the session has completed activation
  122. /// Updates the reachability status and logs the activation state
  123. func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  124. DispatchQueue.main.async {
  125. if let error = error {
  126. print("⌚️ Watch session activation failed: \(error.localizedDescription)")
  127. return
  128. }
  129. print("⌚️ Watch session activated with state: \(activationState.rawValue)")
  130. self.isReachable = session.isReachable
  131. print("⌚️ Watch isReachable after activation: \(session.isReachable)")
  132. }
  133. }
  134. /// Handles incoming messages from the paired iPhone
  135. /// Updates local glucose data, trend, and delta information
  136. func session(_: WCSession, didReceiveMessage message: [String: Any]) {
  137. print("⌚️ Watch received message: \(message)")
  138. DispatchQueue.main.async { [weak self] in
  139. guard let self = self else { return }
  140. if let currentGlucose = message["currentGlucose"] as? String {
  141. self.currentGlucose = currentGlucose
  142. }
  143. if let trend = message["trend"] as? String {
  144. self.trend = trend
  145. }
  146. if let delta = message["delta"] as? String {
  147. self.delta = delta
  148. }
  149. if let iob = message["iob"] as? String {
  150. self.iob = iob
  151. }
  152. if let cob = message["cob"] as? String {
  153. self.cob = cob
  154. }
  155. if let lastLoopTime = message["lastLoopTime"] as? String {
  156. self.lastLoopTime = lastLoopTime
  157. }
  158. if let glucoseData = message["glucoseValues"] as? [[String: Any]] {
  159. self.glucoseValues = glucoseData.compactMap { data in
  160. guard let glucose = data["glucose"] as? Double,
  161. let timestamp = data["date"] as? TimeInterval
  162. else { return nil }
  163. return (Date(timeIntervalSince1970: timestamp), glucose)
  164. }
  165. .sorted { $0.date < $1.date }
  166. }
  167. if let overrideData = message["overridePresets"] as? [[String: Any]] {
  168. self.overridePresets = overrideData.compactMap { data in
  169. guard let name = data["name"] as? String,
  170. let isEnabled = data["isEnabled"] as? Bool
  171. else { return nil }
  172. return OverridePresetWatch(name: name, isEnabled: isEnabled)
  173. }
  174. }
  175. if let tempTargetData = message["tempTargetPresets"] as? [[String: Any]] {
  176. self.tempTargetPresets = tempTargetData.compactMap { data in
  177. guard let name = data["name"] as? String,
  178. let isEnabled = data["isEnabled"] as? Bool
  179. else { return nil }
  180. return TempTargetPresetWatch(name: name, isEnabled: isEnabled)
  181. }
  182. }
  183. }
  184. }
  185. /// Called when the reachability status of the paired iPhone changes
  186. /// Updates the local reachability status
  187. func sessionReachabilityDidChange(_ session: WCSession) {
  188. DispatchQueue.main.async {
  189. self.isReachable = session.isReachable
  190. print("⌚️ Watch reachability changed: \(session.isReachable)")
  191. }
  192. }
  193. }