WatchState.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  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. // Safety limits
  31. var maxBolus: Decimal = 10
  32. var maxCarbs: Decimal = 250
  33. var maxFat: Decimal = 250
  34. var maxProtein: Decimal = 250
  35. var maxIOB: Decimal = 0
  36. var maxCOB: Decimal = 120
  37. // acknowlegement handling
  38. var showCommsAnimation: Bool = false
  39. var showAcknowledgmentBanner: Bool = false
  40. var acknowledgementStatus: AcknowledgementStatus = .pending
  41. var acknowledgmentMessage: String = ""
  42. var shouldNavigateToRoot: Bool = true
  43. // Meal bolus-specific properties
  44. var mealBolusStep: MealBolusStep = .savingCarbs
  45. var isMealBolusCombo: Bool = false
  46. var showBolusProgressOverlay: Bool {
  47. (!showAcknowledgmentBanner || !showCommsAnimation || !showCommsAnimation) && bolusProgress > 0 && bolusProgress < 1.0 &&
  48. !isBolusCanceled
  49. }
  50. override init() {
  51. super.init()
  52. setupSession()
  53. }
  54. /// Configures the WatchConnectivity session if supported on the device
  55. private func setupSession() {
  56. if WCSession.isSupported() {
  57. let session = WCSession.default
  58. session.delegate = self
  59. session.activate()
  60. self.session = session
  61. } else {
  62. print("⌚️ WCSession is not supported on this device")
  63. }
  64. }
  65. // MARK: - Send Data to Phone
  66. /// Sends a bolus insulin request to the paired iPhone
  67. /// - Parameters:
  68. /// - amount: The insulin amount to be delivered
  69. func sendBolusRequest(_ amount: Decimal) {
  70. guard let session = session, session.isReachable else { return }
  71. isBolusCanceled = false // Reset canceled state when starting new bolus
  72. activeBolusAmount = Double(truncating: amount as NSNumber) // Set active bolus amount
  73. let message: [String: Any] = [
  74. "bolus": amount
  75. ]
  76. session.sendMessage(message, replyHandler: nil) { error in
  77. print("Error sending bolus request: \(error.localizedDescription)")
  78. }
  79. // Display pending communication animation
  80. showCommsAnimation = true
  81. }
  82. /// Sends a carbohydrate entry request to the paired iPhone
  83. /// - Parameters:
  84. /// - amount: The amount of carbs in grams
  85. /// - date: The timestamp for the carb entry (defaults to current time)
  86. func sendCarbsRequest(_ amount: Int, _ date: Date = Date()) {
  87. guard let session = session, session.isReachable else { return }
  88. let message: [String: Any] = [
  89. "carbs": amount,
  90. "date": date.timeIntervalSince1970
  91. ]
  92. session.sendMessage(message, replyHandler: nil) { error in
  93. print("Error sending carbs request: \(error.localizedDescription)")
  94. }
  95. // Display pending communication animation
  96. showCommsAnimation = true
  97. }
  98. /// Sends a meal and bolus insulin combo request to the paired iPhone
  99. /// - Parameters:
  100. /// - amount: The insulin amount to be delivered
  101. /// - isExternal: Indicates if the bolus is from an external source
  102. func sendMealBolusComboRequest(carbsAmount _: Decimal, bolusAmount: Decimal, _ date: Date = Date()) {
  103. guard let session = session, session.isReachable else { return }
  104. let message: [String: Any] = [
  105. "bolus": bolusAmount,
  106. "carbs": bolusAmount,
  107. "date": date.timeIntervalSince1970
  108. ]
  109. session.sendMessage(message, replyHandler: nil) { error in
  110. print("Error sending meal bolus combo request: \(error.localizedDescription)")
  111. }
  112. // Display pending communication animation
  113. showCommsAnimation = true
  114. isMealBolusCombo = true
  115. }
  116. func sendCancelOverrideRequest() {
  117. guard let session = session, session.isReachable else { return }
  118. let message: [String: Any] = [
  119. "cancelOverride": true
  120. ]
  121. session.sendMessage(message, replyHandler: nil) { error in
  122. print("⌚️ Error sending cancel override request: \(error.localizedDescription)")
  123. }
  124. // Display pending communication animation
  125. showCommsAnimation = true
  126. }
  127. func sendActivateOverrideRequest(presetName: String) {
  128. guard let session = session, session.isReachable else { return }
  129. let message: [String: Any] = [
  130. "activateOverride": presetName
  131. ]
  132. session.sendMessage(message, replyHandler: nil) { error in
  133. print("⌚️ Error sending activate override request: \(error.localizedDescription)")
  134. }
  135. // Display pending communication animation
  136. showCommsAnimation = true
  137. }
  138. func sendCancelTempTargetRequest() {
  139. guard let session = session, session.isReachable else { return }
  140. let message: [String: Any] = [
  141. "cancelTempTarget": true
  142. ]
  143. session.sendMessage(message, replyHandler: nil) { error in
  144. print("⌚️ Error sending cancel temp target request: \(error.localizedDescription)")
  145. }
  146. // Display pending communication animation
  147. showCommsAnimation = true
  148. }
  149. func sendActivateTempTargetRequest(presetName: String) {
  150. guard let session = session, session.isReachable else { return }
  151. let message: [String: Any] = [
  152. "activateTempTarget": presetName
  153. ]
  154. session.sendMessage(message, replyHandler: nil) { error in
  155. print("⌚️ Error sending activate temp target request: \(error.localizedDescription)")
  156. }
  157. // Display pending communication animation
  158. showCommsAnimation = true
  159. }
  160. func sendCancelBolusRequest() {
  161. isBolusCanceled = true
  162. guard let session = session, session.isReachable else { return }
  163. let message: [String: Any] = [
  164. "cancelBolus": true
  165. ]
  166. session.sendMessage(message, replyHandler: nil) { error in
  167. print("Error sending cancel bolus request: \(error.localizedDescription)")
  168. }
  169. // Reset when cancelled
  170. bolusProgress = 0
  171. activeBolusAmount = 0
  172. // Display pending communication animation
  173. showCommsAnimation = true
  174. }
  175. // MARK: – Handle Acknowledgement Messages FROM Phone
  176. private func handleAcknowledgment(success: Bool, message: String, isFinal: Bool = true) {
  177. if success {
  178. print("⌚️ Acknowledgment received: \(message)")
  179. showCommsAnimation = false // Hide progress animation
  180. acknowledgementStatus = .success
  181. acknowledgmentMessage = "\(message)"
  182. } else {
  183. print("⌚️ Acknowledgment failed: \(message)")
  184. showCommsAnimation = false // Hide progress animation
  185. acknowledgementStatus = .failure
  186. acknowledgmentMessage = "\(message)"
  187. }
  188. if isFinal {
  189. showAcknowledgmentBanner = true
  190. DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
  191. self.showAcknowledgmentBanner = false
  192. }
  193. }
  194. }
  195. // MARK: - WCSessionDelegate
  196. /// Called when the session has completed activation
  197. /// Updates the reachability status and logs the activation state
  198. func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  199. DispatchQueue.main.async {
  200. if let error = error {
  201. print("⌚️ Watch session activation failed: \(error.localizedDescription)")
  202. return
  203. }
  204. print("⌚️ Watch session activated with state: \(activationState.rawValue)")
  205. self.isReachable = session.isReachable
  206. print("⌚️ Watch isReachable after activation: \(session.isReachable)")
  207. }
  208. }
  209. /// Handles incoming messages from the paired iPhone
  210. /// Updates local glucose data, trend, and delta information
  211. func session(_: WCSession, didReceiveMessage message: [String: Any]) {
  212. print("⌚️ Watch received message: \(message)")
  213. DispatchQueue.main.async { [weak self] in
  214. guard let self = self else { return }
  215. if let acknowledged = message["acknowledged"] as? Bool,
  216. let ackMessage = message["message"] as? String
  217. {
  218. switch ackMessage {
  219. case "Saving carbs...":
  220. self.isMealBolusCombo = true
  221. self.mealBolusStep = .savingCarbs
  222. self.showCommsAnimation = true
  223. self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: false)
  224. case "Enacting bolus...":
  225. self.isMealBolusCombo = true
  226. self.mealBolusStep = .enactingBolus
  227. self.showCommsAnimation = true
  228. self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: false)
  229. case "Carbs and bolus logged successfully":
  230. self.isMealBolusCombo = false
  231. self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: true)
  232. default:
  233. self.isMealBolusCombo = false
  234. self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: true)
  235. }
  236. }
  237. if let currentGlucose = message["currentGlucose"] as? String {
  238. self.currentGlucose = currentGlucose
  239. }
  240. if let trend = message["trend"] as? String {
  241. self.trend = trend
  242. }
  243. if let delta = message["delta"] as? String {
  244. self.delta = delta
  245. }
  246. if let iob = message["iob"] as? String {
  247. self.iob = iob
  248. }
  249. if let cob = message["cob"] as? String {
  250. self.cob = cob
  251. }
  252. if let lastLoopTime = message["lastLoopTime"] as? String {
  253. self.lastLoopTime = lastLoopTime
  254. }
  255. if let glucoseData = message["glucoseValues"] as? [[String: Any]] {
  256. self.glucoseValues = glucoseData.compactMap { data in
  257. guard let glucose = data["glucose"] as? Double,
  258. let timestamp = data["date"] as? TimeInterval
  259. else { return nil }
  260. return (Date(timeIntervalSince1970: timestamp), glucose)
  261. }
  262. .sorted { $0.date < $1.date }
  263. }
  264. if let overrideData = message["overridePresets"] as? [[String: Any]] {
  265. self.overridePresets = overrideData.compactMap { data in
  266. guard let name = data["name"] as? String,
  267. let isEnabled = data["isEnabled"] as? Bool
  268. else { return nil }
  269. return OverridePresetWatch(name: name, isEnabled: isEnabled)
  270. }
  271. }
  272. if let tempTargetData = message["tempTargetPresets"] as? [[String: Any]] {
  273. self.tempTargetPresets = tempTargetData.compactMap { data in
  274. guard let name = data["name"] as? String,
  275. let isEnabled = data["isEnabled"] as? Bool
  276. else { return nil }
  277. return TempTargetPresetWatch(name: name, isEnabled: isEnabled)
  278. }
  279. }
  280. if let bolusProgress = message["bolusProgress"] as? Double {
  281. if !self.isBolusCanceled {
  282. self.bolusProgress = bolusProgress
  283. }
  284. }
  285. if let bolusWasCanceled = message["bolusCanceled"] as? Bool, bolusWasCanceled {
  286. self.bolusProgress = 0
  287. self.activeBolusAmount = 0
  288. return
  289. }
  290. // Debug print für die Safety Limits
  291. if let maxBolusValue = message["maxBolus"] {
  292. print("⌚️ Received maxBolus: \(maxBolusValue) of type \(type(of: maxBolusValue))")
  293. if let decimalValue = (maxBolusValue as? NSNumber)?.decimalValue {
  294. self.maxBolus = decimalValue
  295. print("⌚️ Converted maxBolus to: \(decimalValue)")
  296. }
  297. }
  298. if let maxCarbsValue = message["maxCarbs"] {
  299. if let decimalValue = (maxCarbsValue as? NSNumber)?.decimalValue {
  300. self.maxCarbs = decimalValue
  301. }
  302. }
  303. if let maxFatValue = message["maxFat"] {
  304. if let decimalValue = (maxFatValue as? NSNumber)?.decimalValue {
  305. self.maxFat = decimalValue
  306. }
  307. }
  308. if let maxProteinValue = message["maxProtein"] {
  309. if let decimalValue = (maxProteinValue as? NSNumber)?.decimalValue {
  310. self.maxProtein = decimalValue
  311. }
  312. }
  313. if let maxIOBValue = message["maxIOB"] {
  314. if let decimalValue = (maxIOBValue as? NSNumber)?.decimalValue {
  315. self.maxIOB = decimalValue
  316. }
  317. }
  318. if let maxCOBValue = message["maxCOB"] {
  319. if let decimalValue = (maxCOBValue as? NSNumber)?.decimalValue {
  320. self.maxCOB = decimalValue
  321. }
  322. }
  323. }
  324. }
  325. /// Called when the reachability status of the paired iPhone changes
  326. /// Updates the local reachability status
  327. func sessionReachabilityDidChange(_ session: WCSession) {
  328. DispatchQueue.main.async {
  329. self.isReachable = session.isReachable
  330. print("⌚️ Watch reachability changed: \(session.isReachable)")
  331. }
  332. }
  333. }