WatchState.swift 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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 activeBolusAmount = 0.0
  27. var confirmationProgress = 0.0
  28. var bolusProgress: Double = 0.0
  29. var isBolusCanceled = false
  30. override init() {
  31. super.init()
  32. setupSession()
  33. }
  34. /// Configures the WatchConnectivity session if supported on the device
  35. private func setupSession() {
  36. if WCSession.isSupported() {
  37. let session = WCSession.default
  38. session.delegate = self
  39. session.activate()
  40. self.session = session
  41. } else {
  42. print("⌚️ WCSession is not supported on this device")
  43. }
  44. }
  45. // MARK: - Send Data to Phone
  46. /// Sends a bolus insulin request to the paired iPhone
  47. /// - Parameters:
  48. /// - amount: The insulin amount to be delivered
  49. func sendBolusRequest(_ amount: Decimal) {
  50. guard let session = session, session.isReachable else { return }
  51. isBolusCanceled = false // Reset canceled state when starting new bolus
  52. activeBolusAmount = Double(truncating: amount as NSNumber) // Set active bolus amount
  53. let message: [String: Any] = [
  54. "bolus": amount
  55. ]
  56. session.sendMessage(message, replyHandler: nil) { error in
  57. print("Error sending bolus request: \(error.localizedDescription)")
  58. }
  59. }
  60. /// Sends a carbohydrate entry request to the paired iPhone
  61. /// - Parameters:
  62. /// - amount: The amount of carbs in grams
  63. /// - date: The timestamp for the carb entry (defaults to current time)
  64. func sendCarbsRequest(_ amount: Int, _ date: Date = Date()) {
  65. guard let session = session, session.isReachable else { return }
  66. let message: [String: Any] = [
  67. "carbs": amount,
  68. "date": date.timeIntervalSince1970
  69. ]
  70. session.sendMessage(message, replyHandler: nil) { error in
  71. print("Error sending carbs request: \(error.localizedDescription)")
  72. }
  73. }
  74. /// Sends a meal and bolus insulin combo request to the paired iPhone
  75. /// - Parameters:
  76. /// - amount: The insulin amount to be delivered
  77. /// - isExternal: Indicates if the bolus is from an external source
  78. func sendMealBolusComboRequest(carbsAmount _: Decimal, bolusAmount: Decimal, _ date: Date = Date()) {
  79. guard let session = session, session.isReachable else { return }
  80. let message: [String: Any] = [
  81. "bolus": bolusAmount,
  82. "carbs": bolusAmount,
  83. "date": date.timeIntervalSince1970
  84. ]
  85. session.sendMessage(message, replyHandler: nil) { error in
  86. print("Error sending meal bolus combo request: \(error.localizedDescription)")
  87. }
  88. }
  89. func sendCancelOverrideRequest() {
  90. guard let session = session, session.isReachable else { return }
  91. let message: [String: Any] = [
  92. "cancelOverride": true
  93. ]
  94. session.sendMessage(message, replyHandler: nil) { error in
  95. print("⌚️ Error sending cancel override request: \(error.localizedDescription)")
  96. }
  97. }
  98. func sendActivateOverrideRequest(presetName: String) {
  99. guard let session = session, session.isReachable else { return }
  100. let message: [String: Any] = [
  101. "activateOverride": presetName
  102. ]
  103. session.sendMessage(message, replyHandler: nil) { error in
  104. print("⌚️ Error sending activate override request: \(error.localizedDescription)")
  105. }
  106. }
  107. func sendCancelTempTargetRequest() {
  108. guard let session = session, session.isReachable else { return }
  109. let message: [String: Any] = [
  110. "cancelTempTarget": true
  111. ]
  112. session.sendMessage(message, replyHandler: nil) { error in
  113. print("⌚️ Error sending cancel temp target request: \(error.localizedDescription)")
  114. }
  115. }
  116. func sendActivateTempTargetRequest(presetName: String) {
  117. guard let session = session, session.isReachable else { return }
  118. let message: [String: Any] = [
  119. "activateTempTarget": presetName
  120. ]
  121. session.sendMessage(message, replyHandler: nil) { error in
  122. print("⌚️ Error sending activate temp target request: \(error.localizedDescription)")
  123. }
  124. }
  125. func sendCancelBolusRequest() {
  126. isBolusCanceled = true
  127. guard let session = session, session.isReachable else { return }
  128. let message: [String: Any] = [
  129. "cancelBolus": true
  130. ]
  131. session.sendMessage(message, replyHandler: nil) { error in
  132. print("Error sending cancel bolus request: \(error.localizedDescription)")
  133. }
  134. }
  135. // MARK: - WCSessionDelegate
  136. /// Called when the session has completed activation
  137. /// Updates the reachability status and logs the activation state
  138. func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  139. DispatchQueue.main.async {
  140. if let error = error {
  141. print("⌚️ Watch session activation failed: \(error.localizedDescription)")
  142. return
  143. }
  144. print("⌚️ Watch session activated with state: \(activationState.rawValue)")
  145. self.isReachable = session.isReachable
  146. print("⌚️ Watch isReachable after activation: \(session.isReachable)")
  147. }
  148. }
  149. /// Handles incoming messages from the paired iPhone
  150. /// Updates local glucose data, trend, and delta information
  151. func session(_: WCSession, didReceiveMessage message: [String: Any]) {
  152. print("⌚️ Watch received message: \(message)")
  153. DispatchQueue.main.async { [weak self] in
  154. guard let self = self else { return }
  155. if let currentGlucose = message["currentGlucose"] as? String {
  156. self.currentGlucose = currentGlucose
  157. }
  158. if let trend = message["trend"] as? String {
  159. self.trend = trend
  160. }
  161. if let delta = message["delta"] as? String {
  162. self.delta = delta
  163. }
  164. if let iob = message["iob"] as? String {
  165. self.iob = iob
  166. }
  167. if let cob = message["cob"] as? String {
  168. self.cob = cob
  169. }
  170. if let lastLoopTime = message["lastLoopTime"] as? String {
  171. self.lastLoopTime = lastLoopTime
  172. }
  173. if let glucoseData = message["glucoseValues"] as? [[String: Any]] {
  174. self.glucoseValues = glucoseData.compactMap { data in
  175. guard let glucose = data["glucose"] as? Double,
  176. let timestamp = data["date"] as? TimeInterval
  177. else { return nil }
  178. return (Date(timeIntervalSince1970: timestamp), glucose)
  179. }
  180. .sorted { $0.date < $1.date }
  181. }
  182. if let overrideData = message["overridePresets"] as? [[String: Any]] {
  183. self.overridePresets = overrideData.compactMap { data in
  184. guard let name = data["name"] as? String,
  185. let isEnabled = data["isEnabled"] as? Bool
  186. else { return nil }
  187. return OverridePresetWatch(name: name, isEnabled: isEnabled)
  188. }
  189. }
  190. if let tempTargetData = message["tempTargetPresets"] as? [[String: Any]] {
  191. self.tempTargetPresets = tempTargetData.compactMap { data in
  192. guard let name = data["name"] as? String,
  193. let isEnabled = data["isEnabled"] as? Bool
  194. else { return nil }
  195. return TempTargetPresetWatch(name: name, isEnabled: isEnabled)
  196. }
  197. }
  198. if let bolusProgress = message["bolusProgress"] as? Double {
  199. if !self.isBolusCanceled {
  200. self.bolusProgress = bolusProgress
  201. }
  202. }
  203. }
  204. }
  205. /// Called when the reachability status of the paired iPhone changes
  206. /// Updates the local reachability status
  207. func sessionReachabilityDidChange(_ session: WCSession) {
  208. DispatchQueue.main.async {
  209. self.isReachable = session.isReachable
  210. print("⌚️ Watch reachability changed: \(session.isReachable)")
  211. }
  212. }
  213. }