APSManager.swift 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import LoopKit
  5. import LoopKitUI
  6. import OmniBLE
  7. import OmniKit
  8. import RileyLinkKit
  9. import SwiftDate
  10. import Swinject
  11. protocol APSManager {
  12. func heartbeat(date: Date)
  13. func autotune() -> AnyPublisher<Autotune?, Never>
  14. func enactBolus(amount: Double, isSMB: Bool)
  15. var pumpManager: PumpManagerUI? { get set }
  16. var bluetoothManager: BluetoothStateManager? { get }
  17. var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
  18. var pumpName: CurrentValueSubject<String, Never> { get }
  19. var isLooping: CurrentValueSubject<Bool, Never> { get }
  20. var lastLoopDate: Date { get }
  21. var lastLoopDateSubject: PassthroughSubject<Date, Never> { get }
  22. var bolusProgress: CurrentValueSubject<Decimal?, Never> { get }
  23. var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
  24. var isManualTempBasal: Bool { get }
  25. func enactTempBasal(rate: Double, duration: TimeInterval)
  26. func makeProfiles() -> AnyPublisher<Bool, Never>
  27. func determineBasal() -> AnyPublisher<Bool, Never>
  28. func determineBasalSync()
  29. func roundBolus(amount: Decimal) -> Decimal
  30. var lastError: CurrentValueSubject<Error?, Never> { get }
  31. func cancelBolus()
  32. func enactAnnouncement(_ announcement: Announcement)
  33. }
  34. enum APSError: LocalizedError {
  35. case pumpError(Error)
  36. case invalidPumpState(message: String)
  37. case glucoseError(message: String)
  38. case apsError(message: String)
  39. case deviceSyncError(message: String)
  40. case manualBasalTemp(message: String)
  41. var errorDescription: String? {
  42. switch self {
  43. case let .pumpError(error):
  44. return "Pump error: \(error.localizedDescription)"
  45. case let .invalidPumpState(message):
  46. return "Error: Invalid Pump State: \(message)"
  47. case let .glucoseError(message):
  48. return "Error: Invalid glucose: \(message)"
  49. case let .apsError(message):
  50. return "APS error: \(message)"
  51. case let .deviceSyncError(message):
  52. return "Sync error: \(message)"
  53. case let .manualBasalTemp(message):
  54. return "Manual Basal Temp : \(message)"
  55. }
  56. }
  57. }
  58. final class BaseAPSManager: APSManager, Injectable {
  59. private let processQueue = DispatchQueue(label: "BaseAPSManager.processQueue")
  60. @Injected() private var storage: FileStorage!
  61. @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
  62. @Injected() private var alertHistoryStorage: AlertHistoryStorage!
  63. @Injected() private var glucoseStorage: GlucoseStorage!
  64. @Injected() private var tempTargetsStorage: TempTargetsStorage!
  65. @Injected() private var carbsStorage: CarbsStorage!
  66. @Injected() private var announcementsStorage: AnnouncementsStorage!
  67. @Injected() private var deviceDataManager: DeviceDataManager!
  68. @Injected() private var nightscout: NightscoutManager!
  69. @Injected() private var settingsManager: SettingsManager!
  70. @Injected() private var broadcaster: Broadcaster!
  71. @Injected() private var healthKitManager: HealthKitManager!
  72. @Persisted(key: "lastAutotuneDate") private var lastAutotuneDate = Date()
  73. @Persisted(key: "lastStartLoopDate") private var lastStartLoopDate: Date = .distantPast
  74. @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
  75. didSet {
  76. lastLoopDateSubject.send(lastLoopDate)
  77. }
  78. }
  79. let coredataContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
  80. private var openAPS: OpenAPS!
  81. private var lifetime = Lifetime()
  82. private var backGroundTaskID: UIBackgroundTaskIdentifier?
  83. var pumpManager: PumpManagerUI? {
  84. get { deviceDataManager.pumpManager }
  85. set { deviceDataManager.pumpManager = newValue }
  86. }
  87. var bluetoothManager: BluetoothStateManager? { deviceDataManager.bluetoothManager }
  88. @Persisted(key: "isManualTempBasal") var isManualTempBasal: Bool = false
  89. let isLooping = CurrentValueSubject<Bool, Never>(false)
  90. let lastLoopDateSubject = PassthroughSubject<Date, Never>()
  91. let lastError = CurrentValueSubject<Error?, Never>(nil)
  92. let bolusProgress = CurrentValueSubject<Decimal?, Never>(nil)
  93. var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> {
  94. deviceDataManager.pumpDisplayState
  95. }
  96. var pumpName: CurrentValueSubject<String, Never> {
  97. deviceDataManager.pumpName
  98. }
  99. var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> {
  100. deviceDataManager.pumpExpiresAtDate
  101. }
  102. var settings: FreeAPSSettings {
  103. get { settingsManager.settings }
  104. set { settingsManager.settings = newValue }
  105. }
  106. init(resolver: Resolver) {
  107. injectServices(resolver)
  108. openAPS = OpenAPS(storage: storage)
  109. subscribe()
  110. lastLoopDateSubject.send(lastLoopDate)
  111. isLooping
  112. .weakAssign(to: \.deviceDataManager.loopInProgress, on: self)
  113. .store(in: &lifetime)
  114. }
  115. private func subscribe() {
  116. deviceDataManager.recommendsLoop
  117. .receive(on: processQueue)
  118. .sink { [weak self] in
  119. self?.loop()
  120. }
  121. .store(in: &lifetime)
  122. pumpManager?.addStatusObserver(self, queue: processQueue)
  123. deviceDataManager.errorSubject
  124. .receive(on: processQueue)
  125. .map { APSError.pumpError($0) }
  126. .sink {
  127. self.processError($0)
  128. }
  129. .store(in: &lifetime)
  130. deviceDataManager.bolusTrigger
  131. .receive(on: processQueue)
  132. .sink { bolusing in
  133. if bolusing {
  134. self.createBolusReporter()
  135. } else {
  136. self.clearBolusReporter()
  137. }
  138. }
  139. .store(in: &lifetime)
  140. // manage a manual Temp Basal from OmniPod - Force loop() after stop a temp basal or finished
  141. deviceDataManager.manualTempBasal
  142. .receive(on: processQueue)
  143. .sink { manualBasal in
  144. if manualBasal {
  145. self.isManualTempBasal = true
  146. } else {
  147. if self.isManualTempBasal {
  148. self.isManualTempBasal = false
  149. self.loop()
  150. }
  151. }
  152. }
  153. .store(in: &lifetime)
  154. }
  155. func heartbeat(date: Date) {
  156. deviceDataManager.heartbeat(date: date)
  157. }
  158. // Loop entry point
  159. private func loop() {
  160. // check the last start of looping is more the loopInterval but the previous loop was completed
  161. if lastLoopDate > lastStartLoopDate {
  162. guard lastStartLoopDate.addingTimeInterval(Config.loopInterval) < Date() else {
  163. debug(.apsManager, "too close to do a loop : \(lastStartLoopDate)")
  164. return
  165. }
  166. }
  167. guard !isLooping.value else {
  168. warning(.apsManager, "Loop already in progress. Skip recommendation.")
  169. return
  170. }
  171. // start background time extension
  172. backGroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Loop starting") {
  173. guard let backgroundTask = self.backGroundTaskID else { return }
  174. UIApplication.shared.endBackgroundTask(backgroundTask)
  175. self.backGroundTaskID = .invalid
  176. }
  177. debug(.apsManager, "Starting loop with a delay of \(UIApplication.shared.backgroundTimeRemaining.rounded())")
  178. lastStartLoopDate = Date()
  179. var loopStatRecord = LoopStats(
  180. start: lastStartLoopDate,
  181. loopStatus: "Starting"
  182. )
  183. isLooping.send(true)
  184. determineBasal()
  185. .replaceEmpty(with: false)
  186. .flatMap { [weak self] success -> AnyPublisher<Void, Error> in
  187. guard let self = self, success else {
  188. return Fail(error: APSError.apsError(message: "Determine basal failed")).eraseToAnyPublisher()
  189. }
  190. // Open loop completed
  191. guard self.settings.closedLoop else {
  192. self.nightscout.uploadStatus()
  193. return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
  194. }
  195. self.nightscout.uploadStatus()
  196. // Closed loop - enact suggested
  197. return self.enactSuggested()
  198. }
  199. .sink { [weak self] completion in
  200. guard let self = self else { return }
  201. loopStatRecord.end = Date()
  202. loopStatRecord.duration = self.roundDouble(
  203. (loopStatRecord.end! - loopStatRecord.start).timeInterval / 60,
  204. 2
  205. )
  206. if case let .failure(error) = completion {
  207. loopStatRecord.loopStatus = error.localizedDescription
  208. self.loopCompleted(error: error, loopStatRecord: loopStatRecord)
  209. } else {
  210. loopStatRecord.loopStatus = "Success"
  211. self.loopCompleted(loopStatRecord: loopStatRecord)
  212. }
  213. } receiveValue: {}
  214. .store(in: &lifetime)
  215. }
  216. // Loop exit point
  217. private func loopCompleted(error: Error? = nil, loopStatRecord: LoopStats) {
  218. isLooping.send(false)
  219. // save AH events
  220. let events = pumpHistoryStorage.recent()
  221. healthKitManager.saveIfNeeded(pumpEvents: events)
  222. if let error = error {
  223. warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
  224. if let backgroundTask = backGroundTaskID {
  225. UIApplication.shared.endBackgroundTask(backgroundTask)
  226. backGroundTaskID = .invalid
  227. }
  228. processError(error)
  229. } else {
  230. debug(.apsManager, "Loop succeeded")
  231. lastLoopDate = Date()
  232. lastError.send(nil)
  233. }
  234. loopStats(loopStatRecord: loopStatRecord)
  235. if settings.closedLoop {
  236. reportEnacted(received: error == nil)
  237. }
  238. // end of the BG tasks
  239. if let backgroundTask = backGroundTaskID {
  240. UIApplication.shared.endBackgroundTask(backgroundTask)
  241. backGroundTaskID = .invalid
  242. }
  243. }
  244. private func verifyStatus() -> Error? {
  245. guard let pump = pumpManager else {
  246. return APSError.invalidPumpState(message: "Pump not set")
  247. }
  248. let status = pump.status.pumpStatus
  249. guard !status.bolusing else {
  250. return APSError.invalidPumpState(message: "Pump is bolusing")
  251. }
  252. guard !status.suspended else {
  253. return APSError.invalidPumpState(message: "Pump suspended")
  254. }
  255. let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
  256. guard reservoir >= 0 else {
  257. return APSError.invalidPumpState(message: "Reservoir is empty")
  258. }
  259. return nil
  260. }
  261. private func autosens() -> AnyPublisher<Bool, Never> {
  262. guard let autosens = storage.retrieve(OpenAPS.Settings.autosense, as: Autosens.self),
  263. (autosens.timestamp ?? .distantPast).addingTimeInterval(30.minutes.timeInterval) > Date()
  264. else {
  265. return openAPS.autosense()
  266. .map { $0 != nil }
  267. .eraseToAnyPublisher()
  268. }
  269. return Just(false).eraseToAnyPublisher()
  270. }
  271. func determineBasal() -> AnyPublisher<Bool, Never> {
  272. debug(.apsManager, "Start determine basal")
  273. guard let glucose = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.isNotEmpty else {
  274. debug(.apsManager, "Not enough glucose data")
  275. processError(APSError.glucoseError(message: "Not enough glucose data"))
  276. return Just(false).eraseToAnyPublisher()
  277. }
  278. let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
  279. guard lastGlucoseDate >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
  280. debug(.apsManager, "Glucose data is stale")
  281. processError(APSError.glucoseError(message: "Glucose data is stale"))
  282. return Just(false).eraseToAnyPublisher()
  283. }
  284. guard glucoseStorage.isGlucoseNotFlat() else {
  285. debug(.apsManager, "Glucose data is too flat")
  286. processError(APSError.glucoseError(message: "Glucose data is too flat"))
  287. return Just(false).eraseToAnyPublisher()
  288. }
  289. let now = Date()
  290. let temp = currentTemp(date: now)
  291. let mainPublisher = makeProfiles()
  292. .flatMap { _ in self.autosens() }
  293. .flatMap { _ in self.dailyAutotune() }
  294. .flatMap { _ in self.openAPS.determineBasal(currentTemp: temp, clock: now) }
  295. .map { suggestion -> Bool in
  296. if let suggestion = suggestion {
  297. DispatchQueue.main.async {
  298. self.broadcaster.notify(SuggestionObserver.self, on: .main) {
  299. $0.suggestionDidUpdate(suggestion)
  300. }
  301. }
  302. }
  303. return suggestion != nil
  304. }
  305. .eraseToAnyPublisher()
  306. if temp.duration == 0,
  307. settings.closedLoop,
  308. settingsManager.preferences.unsuspendIfNoTemp,
  309. let pump = pumpManager,
  310. pump.status.pumpStatus.suspended
  311. {
  312. return pump.resumeDelivery()
  313. .flatMap { _ in mainPublisher }
  314. .replaceError(with: false)
  315. .eraseToAnyPublisher()
  316. }
  317. return mainPublisher
  318. }
  319. func determineBasalSync() {
  320. determineBasal().cancellable().store(in: &lifetime)
  321. }
  322. func makeProfiles() -> AnyPublisher<Bool, Never> {
  323. openAPS.makeProfiles(useAutotune: settings.useAutotune)
  324. .map { tunedProfile in
  325. if let basalProfile = tunedProfile?.basalProfile {
  326. self.processQueue.async {
  327. self.broadcaster.notify(BasalProfileObserver.self, on: self.processQueue) {
  328. $0.basalProfileDidChange(basalProfile)
  329. }
  330. }
  331. }
  332. return tunedProfile != nil
  333. }
  334. .eraseToAnyPublisher()
  335. }
  336. func roundBolus(amount: Decimal) -> Decimal {
  337. guard let pump = pumpManager else { return amount }
  338. let rounded = Decimal(pump.roundToSupportedBolusVolume(units: Double(amount)))
  339. let maxBolus = Decimal(pump.roundToSupportedBolusVolume(units: Double(settingsManager.pumpSettings.maxBolus)))
  340. return min(rounded, maxBolus)
  341. }
  342. private var bolusReporter: DoseProgressReporter?
  343. func enactBolus(amount: Double, isSMB: Bool) {
  344. if let error = verifyStatus() {
  345. processError(error)
  346. processQueue.async {
  347. self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
  348. $0.bolusDidFail()
  349. }
  350. }
  351. return
  352. }
  353. guard let pump = pumpManager else { return }
  354. let roundedAmout = pump.roundToSupportedBolusVolume(units: amount)
  355. debug(.apsManager, "Enact bolus \(roundedAmout), manual \(!isSMB)")
  356. pump.enactBolus(units: roundedAmout, automatic: isSMB).sink { completion in
  357. if case let .failure(error) = completion {
  358. warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
  359. self.processError(APSError.pumpError(error))
  360. if !isSMB {
  361. self.processQueue.async {
  362. self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
  363. $0.bolusDidFail()
  364. }
  365. }
  366. }
  367. } else {
  368. debug(.apsManager, "Bolus succeeded")
  369. if !isSMB {
  370. self.determineBasal().sink { _ in }.store(in: &self.lifetime)
  371. }
  372. self.bolusProgress.send(0)
  373. }
  374. } receiveValue: { _ in }
  375. .store(in: &lifetime)
  376. }
  377. func cancelBolus() {
  378. guard let pump = pumpManager, pump.status.pumpStatus.bolusing else { return }
  379. debug(.apsManager, "Cancel bolus")
  380. pump.cancelBolus().sink { completion in
  381. if case let .failure(error) = completion {
  382. debug(.apsManager, "Bolus cancellation failed with error: \(error.localizedDescription)")
  383. self.processError(APSError.pumpError(error))
  384. } else {
  385. debug(.apsManager, "Bolus cancelled")
  386. }
  387. self.bolusReporter?.removeObserver(self)
  388. self.bolusReporter = nil
  389. self.bolusProgress.send(nil)
  390. } receiveValue: { _ in }
  391. .store(in: &lifetime)
  392. }
  393. func enactTempBasal(rate: Double, duration: TimeInterval) {
  394. if let error = verifyStatus() {
  395. processError(error)
  396. return
  397. }
  398. guard let pump = pumpManager else { return }
  399. // unable to do temp basal during manual temp basal 😁
  400. if isManualTempBasal {
  401. processError(APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  402. return
  403. }
  404. debug(.apsManager, "Enact temp basal \(rate) - \(duration)")
  405. let roundedAmout = pump.roundToSupportedBasalRate(unitsPerHour: rate)
  406. pump.enactTempBasal(unitsPerHour: roundedAmout, for: duration) { error in
  407. if let error = error {
  408. debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
  409. self.processError(APSError.pumpError(error))
  410. } else {
  411. debug(.apsManager, "Temp Basal succeeded")
  412. let temp = TempBasal(duration: Int(duration / 60), rate: Decimal(rate), temp: .absolute, timestamp: Date())
  413. self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
  414. if rate == 0, duration == 0 {
  415. self.pumpHistoryStorage.saveCancelTempEvents()
  416. }
  417. }
  418. }
  419. }
  420. func dailyAutotune() -> AnyPublisher<Bool, Never> {
  421. guard settings.useAutotune else {
  422. return Just(false).eraseToAnyPublisher()
  423. }
  424. let now = Date()
  425. guard lastAutotuneDate.isBeforeDate(now, granularity: .day) else {
  426. return Just(false).eraseToAnyPublisher()
  427. }
  428. lastAutotuneDate = now
  429. return autotune().map { $0 != nil }.eraseToAnyPublisher()
  430. }
  431. func autotune() -> AnyPublisher<Autotune?, Never> {
  432. openAPS.autotune().eraseToAnyPublisher()
  433. }
  434. func enactAnnouncement(_ announcement: Announcement) {
  435. guard let action = announcement.action else {
  436. warning(.apsManager, "Invalid Announcement action")
  437. return
  438. }
  439. guard let pump = pumpManager else {
  440. warning(.apsManager, "Pump is not set")
  441. return
  442. }
  443. debug(.apsManager, "Start enact announcement: \(action)")
  444. switch action {
  445. case let .bolus(amount):
  446. if let error = verifyStatus() {
  447. processError(error)
  448. return
  449. }
  450. let roundedAmount = pump.roundToSupportedBolusVolume(units: Double(amount))
  451. pump.enactBolus(units: roundedAmount, activationType: .manualRecommendationAccepted) { error in
  452. if let error = error {
  453. // warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
  454. switch error {
  455. case .uncertainDelivery:
  456. // Do not generate notification on uncertain delivery error
  457. break
  458. default:
  459. // Do not generate notifications for automatic boluses that fail.
  460. warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
  461. }
  462. } else {
  463. debug(.apsManager, "Announcement Bolus succeeded")
  464. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  465. self.bolusProgress.send(0)
  466. }
  467. }
  468. case let .pump(pumpAction):
  469. switch pumpAction {
  470. case .suspend:
  471. if let error = verifyStatus() {
  472. processError(error)
  473. return
  474. }
  475. pump.suspendDelivery { error in
  476. if let error = error {
  477. debug(.apsManager, "Pump not suspended by Announcement: \(error.localizedDescription)")
  478. } else {
  479. debug(.apsManager, "Pump suspended by Announcement")
  480. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  481. self.nightscout.uploadStatus()
  482. }
  483. }
  484. case .resume:
  485. guard pump.status.pumpStatus.suspended else {
  486. return
  487. }
  488. pump.resumeDelivery { error in
  489. if let error = error {
  490. warning(.apsManager, "Pump not resumed by Announcement: \(error.localizedDescription)")
  491. } else {
  492. debug(.apsManager, "Pump resumed by Announcement")
  493. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  494. self.nightscout.uploadStatus()
  495. }
  496. }
  497. }
  498. case let .looping(closedLoop):
  499. settings.closedLoop = closedLoop
  500. debug(.apsManager, "Closed loop \(closedLoop) by Announcement")
  501. announcementsStorage.storeAnnouncements([announcement], enacted: true)
  502. case let .tempbasal(rate, duration):
  503. if let error = verifyStatus() {
  504. processError(error)
  505. return
  506. }
  507. // unable to do temp basal during manual temp basal 😁
  508. if isManualTempBasal {
  509. processError(APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  510. return
  511. }
  512. guard !settings.closedLoop else {
  513. return
  514. }
  515. let roundedRate = pump.roundToSupportedBasalRate(unitsPerHour: Double(rate))
  516. pump.enactTempBasal(unitsPerHour: roundedRate, for: TimeInterval(duration) * 60) { error in
  517. if let error = error {
  518. warning(.apsManager, "Announcement TempBasal failed with error: \(error.localizedDescription)")
  519. } else {
  520. debug(.apsManager, "Announcement TempBasal succeeded")
  521. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  522. }
  523. }
  524. }
  525. }
  526. private func currentTemp(date: Date) -> TempBasal {
  527. let defaultTemp = { () -> TempBasal in
  528. guard let temp = storage.retrieve(OpenAPS.Monitor.tempBasal, as: TempBasal.self) else {
  529. return TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: Date())
  530. }
  531. let delta = Int((date.timeIntervalSince1970 - temp.timestamp.timeIntervalSince1970) / 60)
  532. let duration = max(0, temp.duration - delta)
  533. return TempBasal(duration: duration, rate: temp.rate, temp: .absolute, timestamp: date)
  534. }()
  535. guard let state = pumpManager?.status.basalDeliveryState else { return defaultTemp }
  536. switch state {
  537. case .active:
  538. return TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: date)
  539. case let .tempBasal(dose):
  540. let rate = Decimal(dose.unitsPerHour)
  541. let durationMin = max(0, Int((dose.endDate.timeIntervalSince1970 - date.timeIntervalSince1970) / 60))
  542. return TempBasal(duration: durationMin, rate: rate, temp: .absolute, timestamp: date)
  543. default:
  544. return defaultTemp
  545. }
  546. }
  547. private func enactSuggested() -> AnyPublisher<Void, Error> {
  548. guard let suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) else {
  549. return Fail(error: APSError.apsError(message: "Suggestion not found")).eraseToAnyPublisher()
  550. }
  551. guard Date().timeIntervalSince(suggested.deliverAt ?? .distantPast) < Config.eхpirationInterval else {
  552. return Fail(error: APSError.apsError(message: "Suggestion expired")).eraseToAnyPublisher()
  553. }
  554. guard let pump = pumpManager else {
  555. return Fail(error: APSError.apsError(message: "Pump not set")).eraseToAnyPublisher()
  556. }
  557. // unable to do temp basal during manual temp basal 😁
  558. if isManualTempBasal {
  559. return Fail(error: APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  560. .eraseToAnyPublisher()
  561. }
  562. let basalPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
  563. if let error = self.verifyStatus() {
  564. return Fail(error: error).eraseToAnyPublisher()
  565. }
  566. guard let rate = suggested.rate, let duration = suggested.duration else {
  567. // It is OK, no temp required
  568. debug(.apsManager, "No temp required")
  569. return Just(()).setFailureType(to: Error.self)
  570. .eraseToAnyPublisher()
  571. }
  572. return pump.enactTempBasal(unitsPerHour: Double(rate), for: TimeInterval(duration * 60)).map { _ in
  573. let temp = TempBasal(duration: duration, rate: rate, temp: .absolute, timestamp: Date())
  574. self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
  575. return ()
  576. }
  577. .eraseToAnyPublisher()
  578. }.eraseToAnyPublisher()
  579. let bolusPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
  580. if let error = self.verifyStatus() {
  581. return Fail(error: error).eraseToAnyPublisher()
  582. }
  583. guard let units = suggested.units else {
  584. // It is OK, no bolus required
  585. debug(.apsManager, "No bolus required")
  586. return Just(()).setFailureType(to: Error.self)
  587. .eraseToAnyPublisher()
  588. }
  589. return pump.enactBolus(units: Double(units), automatic: true).map { _ in
  590. self.bolusProgress.send(0)
  591. return ()
  592. }
  593. .eraseToAnyPublisher()
  594. }.eraseToAnyPublisher()
  595. return basalPublisher.flatMap { bolusPublisher }.eraseToAnyPublisher()
  596. }
  597. private func reportEnacted(received: Bool) {
  598. if let suggestion = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self), suggestion.deliverAt != nil {
  599. var enacted = suggestion
  600. enacted.timestamp = Date()
  601. enacted.recieved = received
  602. storage.save(enacted, as: OpenAPS.Enact.enacted)
  603. debug(.apsManager, "Suggestion enacted. Received: \(received)")
  604. DispatchQueue.main.async {
  605. self.broadcaster.notify(EnactedSuggestionObserver.self, on: .main) {
  606. $0.enactedSuggestionDidUpdate(enacted)
  607. }
  608. }
  609. nightscout.uploadStatus()
  610. statistics()
  611. }
  612. }
  613. private func roundDecimal(_ decimal: Decimal, _ digits: Double) -> Decimal {
  614. let rounded = round(Double(decimal) * pow(10, digits)) / pow(10, digits)
  615. return Decimal(rounded)
  616. }
  617. private func roundDouble(_ double: Double, _ digits: Double) -> Double {
  618. let rounded = round(Double(double) * pow(10, digits)) / pow(10, digits)
  619. return rounded
  620. }
  621. private func medianCalculation(array: [Double]) -> Double {
  622. guard !array.isEmpty else {
  623. return 0
  624. }
  625. let sorted = array.sorted()
  626. let length = array.count
  627. if length % 2 == 0 {
  628. return (sorted[length / 2 - 1] + sorted[length / 2]) / 2
  629. }
  630. return sorted[length / 2]
  631. }
  632. // Add to statistics.JSON
  633. private func statistics() {
  634. let now = Date()
  635. if settingsManager.settings.uploadStats {
  636. let hour = Calendar.current.component(.hour, from: now)
  637. guard hour > 20 else {
  638. return
  639. }
  640. coredataContext.performAndWait { [self] in
  641. var stats = [StatsData]()
  642. let requestStats = StatsData.fetchRequest() as NSFetchRequest<StatsData>
  643. let sortStats = NSSortDescriptor(key: "lastrun", ascending: false)
  644. requestStats.sortDescriptors = [sortStats]
  645. requestStats.fetchLimit = 1
  646. try? stats = coredataContext.fetch(requestStats)
  647. // Only save and upload once per day
  648. guard (-1 * (stats.first?.lastrun ?? .distantPast).timeIntervalSinceNow.hours) > 22 else { return }
  649. let units = self.settingsManager.settings.units
  650. let preferences = settingsManager.preferences
  651. var carbs = [Carbohydrates]()
  652. var carbTotal: Decimal = 0
  653. let requestCarbs = Carbohydrates.fetchRequest() as NSFetchRequest<Carbohydrates>
  654. let daysAgo = Date().addingTimeInterval(-1.days.timeInterval)
  655. requestCarbs.predicate = NSPredicate(format: "carbs > 0 AND date > %@", daysAgo as NSDate)
  656. let sortCarbs = NSSortDescriptor(key: "date", ascending: true)
  657. requestCarbs.sortDescriptors = [sortCarbs]
  658. try? carbs = coredataContext.fetch(requestCarbs)
  659. carbTotal = carbs.map({ carbs in carbs.carbs as? Decimal ?? 0 }).reduce(0, +)
  660. // MARK: Fetch TDD from CoreData
  661. var tdds = [TDD]()
  662. var currentTDD: Decimal = 0
  663. var tddTotalAverage: Decimal = 0
  664. let requestTDD = TDD.fetchRequest() as NSFetchRequest<TDD>
  665. let sort = NSSortDescriptor(key: "timestamp", ascending: false)
  666. let daysOf14Ago = Date().addingTimeInterval(-14.days.timeInterval)
  667. requestTDD.predicate = NSPredicate(format: "timestamp > %@", daysOf14Ago as NSDate)
  668. requestTDD.sortDescriptors = [sort]
  669. try? tdds = coredataContext.fetch(requestTDD)
  670. if !tdds.isEmpty {
  671. currentTDD = tdds[0].tdd?.decimalValue ?? 0
  672. let tddArray = tdds.compactMap({ insulin in insulin.tdd as? Decimal ?? 0 })
  673. tddTotalAverage = tddArray.reduce(0, +) / Decimal(tddArray.count)
  674. }
  675. var algo_ = "Oref0"
  676. if preferences.sigmoid, preferences.enableDynamicCR {
  677. algo_ = "Dynamic ISF + CR: Sigmoid"
  678. } else if preferences.sigmoid, !preferences.enableDynamicCR {
  679. algo_ = "Dynamic ISF: Sigmoid"
  680. } else if preferences.useNewFormula, preferences.enableDynamicCR {
  681. algo_ = "Dynamic ISF + CR: Logarithmic"
  682. } else if preferences.useNewFormula, !preferences.sigmoid,!preferences.enableDynamicCR {
  683. algo_ = "Dynamic ISF: Logarithmic"
  684. }
  685. let af = preferences.adjustmentFactor
  686. let insulin_type = preferences.curve
  687. let buildDate = Bundle.main.buildDate
  688. let version = Bundle.main.releaseVersionNumber
  689. let build = Bundle.main.buildVersionNumber
  690. let branch = Bundle.main.infoDictionary?["BuildBranch"] as? String ?? ""
  691. let copyrightNotice_ = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
  692. let pump_ = pumpManager?.localizedTitle ?? ""
  693. let cgm = settingsManager.settings.cgm
  694. let file = OpenAPS.Monitor.statistics
  695. var iPa: Decimal = 75
  696. if preferences.useCustomPeakTime {
  697. iPa = preferences.insulinPeakTime
  698. } else if preferences.curve.rawValue == "rapid-acting" {
  699. iPa = 65
  700. } else if preferences.curve.rawValue == "ultra-rapid" {
  701. iPa = 50
  702. }
  703. var lsr = [LoopStatRecord]()
  704. var successRate: Double?
  705. var successNR = 0
  706. var errorNR = 0
  707. var minimumInt = 999.0
  708. var maximumInt = 0.0
  709. var minimumLoopTime = 9999.0
  710. var maximumLoopTime = 0.0
  711. var timeIntervalLoops = 0.0
  712. var previousTimeLoop = Date()
  713. var timeForOneLoop = 0.0
  714. var averageLoopTime = 0.0
  715. var timeForOneLoopArray: [Double] = []
  716. var medianLoopTime = 0.0
  717. var timeIntervalLoopArray: [Double] = []
  718. var medianInterval = 0.0
  719. var averageIntervalLoops = 0.0
  720. var averageLoopDuration = 0.0
  721. let requestLSR = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
  722. requestLSR.predicate = NSPredicate(
  723. format: "start > %@",
  724. Date().addingTimeInterval(-24.hours.timeInterval) as NSDate
  725. )
  726. let sortLSR = NSSortDescriptor(key: "start", ascending: false)
  727. requestLSR.sortDescriptors = [sortLSR]
  728. try? lsr = coredataContext.fetch(requestLSR)
  729. if lsr.isNotEmpty {
  730. var i = 0.0
  731. if let loopEnd = lsr[0].end {
  732. previousTimeLoop = loopEnd
  733. }
  734. for each in lsr {
  735. if let loopEnd = each.end {
  736. let loopDuration = each.duration
  737. if each.loopStatus!.contains("Success") {
  738. successNR += 1
  739. } else {
  740. errorNR += 1
  741. }
  742. i += 1
  743. timeIntervalLoops = (previousTimeLoop - (each.start ?? previousTimeLoop)).timeInterval / 60
  744. if timeIntervalLoops > 0.0, i != 1 {
  745. timeIntervalLoopArray.append(timeIntervalLoops)
  746. }
  747. if timeIntervalLoops > maximumInt {
  748. maximumInt = timeIntervalLoops
  749. }
  750. if timeIntervalLoops < minimumInt, i != 1 {
  751. minimumInt = timeIntervalLoops
  752. }
  753. timeForOneLoop = loopDuration
  754. timeForOneLoopArray.append(timeForOneLoop)
  755. if timeForOneLoop >= maximumLoopTime, timeForOneLoop != 0.0 {
  756. maximumLoopTime = timeForOneLoop
  757. }
  758. if timeForOneLoop <= minimumLoopTime, timeForOneLoop != 0.0 {
  759. minimumLoopTime = timeForOneLoop
  760. }
  761. previousTimeLoop = loopEnd
  762. }
  763. }
  764. successRate = (Double(successNR) / Double(i)) * 100
  765. // Average Loop Interval in minutes
  766. let timeOfFirstIndex = lsr[0].start ?? Date()
  767. let lastIndexWithTimestamp = lsr.count - 1
  768. let timeOfLastIndex = lsr[lastIndexWithTimestamp].end ?? Date()
  769. averageLoopTime = (timeOfFirstIndex - timeOfLastIndex).timeInterval / 60 / Double(errorNR + successNR)
  770. // Median values
  771. medianLoopTime = medianCalculation(array: timeForOneLoopArray)
  772. medianInterval = medianCalculation(array: timeIntervalLoopArray)
  773. // Average time interval between loops
  774. averageIntervalLoops = timeIntervalLoopArray.reduce(0, +) / Double(timeIntervalLoopArray.count)
  775. // Average loop duration
  776. averageLoopDuration = timeForOneLoopArray.reduce(0, +) / Double(timeForOneLoopArray.count)
  777. }
  778. if minimumInt == 999.0 {
  779. minimumInt = 0.0
  780. }
  781. if minimumLoopTime == 9999.0 {
  782. minimumLoopTime = 0.0
  783. }
  784. var glucose = [Readings]()
  785. var firstElementTime = Date()
  786. var lastElementTime = Date()
  787. var currentIndexTime = Date()
  788. var bg: Decimal = 0
  789. var bgArray: [Double] = []
  790. var bgArray_1_: [Double] = []
  791. var bgArray_7_: [Double] = []
  792. var bgArray_30_: [Double] = []
  793. var bgArrayForTIR: [(bg_: Double, date_: Date)] = []
  794. var bgArray_1: [(bg_: Double, date_: Date)] = []
  795. var bgArray_7: [(bg_: Double, date_: Date)] = []
  796. var bgArray_30: [(bg_: Double, date_: Date)] = []
  797. var medianBG = 0.0
  798. var nr_bgs: Decimal = 0
  799. var bg_1: Decimal = 0
  800. var bg_7: Decimal = 0
  801. var bg_30: Decimal = 0
  802. var bg_total: Decimal = 0
  803. var j = -1
  804. var conversionFactor: Decimal = 1
  805. if units == .mmolL {
  806. conversionFactor = 0.0555
  807. }
  808. var numberOfDays: Double = 0
  809. var nr1: Decimal = 0
  810. let requestGFS = Readings.fetchRequest() as NSFetchRequest<Readings>
  811. let sortGlucose = NSSortDescriptor(key: "date", ascending: false)
  812. requestGFS.sortDescriptors = [sortGlucose]
  813. try? glucose = coredataContext.fetch(requestGFS)
  814. // Time In Range (%) and Average Glucose. This will be refactored later after some testing.
  815. let endIndex = glucose.count - 1
  816. firstElementTime = glucose[0].date ?? Date()
  817. lastElementTime = glucose[endIndex].date ?? Date()
  818. currentIndexTime = firstElementTime
  819. numberOfDays = (firstElementTime - lastElementTime).timeInterval / 8.64E4
  820. // Make arrays for median calculations and calculate averages
  821. if endIndex >= 0, (glucose.first?.glucose ?? 0) != 0 {
  822. repeat {
  823. j += 1
  824. if glucose[j].glucose > 0 {
  825. currentIndexTime = glucose[j].date ?? firstElementTime
  826. bg += Decimal(glucose[j].glucose) * conversionFactor
  827. bgArray.append(Double(glucose[j].glucose) * Double(conversionFactor))
  828. bgArrayForTIR.append((Double(glucose[j].glucose), glucose[j].date!))
  829. nr_bgs += 1
  830. if (firstElementTime - currentIndexTime).timeInterval <= 8.64E4 { // 1 day
  831. bg_1 = bg / nr_bgs
  832. bgArray_1 = bgArrayForTIR
  833. bgArray_1_ = bgArray
  834. nr1 = nr_bgs
  835. }
  836. if (firstElementTime - currentIndexTime).timeInterval <= 6.048E5 { // 7 days
  837. bg_7 = bg / nr_bgs
  838. bgArray_7 = bgArrayForTIR
  839. bgArray_7_ = bgArray
  840. }
  841. if (firstElementTime - currentIndexTime).timeInterval <= 2.592E6 { // 30 days
  842. bg_30 = bg / nr_bgs
  843. bgArray_30 = bgArrayForTIR
  844. bgArray_30_ = bgArray
  845. }
  846. }
  847. } while j != glucose.count - 1
  848. } else { return }
  849. if nr_bgs > 0 {
  850. // Up to 91 days
  851. bg_total = bg / nr_bgs
  852. }
  853. // Total median
  854. medianBG = medianCalculation(array: bgArray)
  855. func tir(_ array: [(bg_: Double, date_: Date)]) -> (TIR: Double, hypos: Double, hypers: Double) {
  856. var timeInHypo = 0.0
  857. var timeInHyper = 0.0
  858. var hypos = 0.0
  859. var hypers = 0.0
  860. var i = -1
  861. var lastIndex = false
  862. let endIndex = array.count - 1
  863. let hypoLimit = settingsManager.settings.low
  864. let hyperLimit = settingsManager.settings.high
  865. var full_time = 0.0
  866. if endIndex > 0 {
  867. full_time = (array[0].date_ - array[endIndex].date_).timeInterval
  868. }
  869. while i < endIndex {
  870. i += 1
  871. let currentTime = array[i].date_
  872. var previousTime = currentTime
  873. if i + 1 <= endIndex {
  874. previousTime = array[i + 1].date_
  875. } else {
  876. lastIndex = true
  877. }
  878. if array[i].bg_ < Double(hypoLimit), !lastIndex {
  879. // Exclude duration between CGM readings which are more than 30 minutes
  880. timeInHypo += min((currentTime - previousTime).timeInterval, 30.minutes.timeInterval)
  881. } else if array[i].bg_ >= Double(hyperLimit), !lastIndex {
  882. timeInHyper += min((currentTime - previousTime).timeInterval, 30.minutes.timeInterval)
  883. }
  884. }
  885. if timeInHypo == 0.0 {
  886. hypos = 0
  887. } else if full_time != 0.0 { hypos = (timeInHypo / full_time) * 100
  888. }
  889. if timeInHyper == 0.0 {
  890. hypers = 0
  891. } else if full_time != 0.0 { hypers = (timeInHyper / full_time) * 100
  892. }
  893. let TIR = 100 - (hypos + hypers)
  894. return (roundDouble(TIR, 1), roundDouble(hypos, 1), roundDouble(hypers, 1))
  895. }
  896. // HbA1c estimation (%, mmol/mol) 1 day
  897. var NGSPa1CStatisticValue: Decimal = 0.0
  898. var IFCCa1CStatisticValue: Decimal = 0.0
  899. if nr_bgs > 0 {
  900. NGSPa1CStatisticValue = ((bg_1 / conversionFactor) + 46.7) / 28.7 // NGSP (%)
  901. IFCCa1CStatisticValue = 10.929 *
  902. (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol) A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
  903. }
  904. // 7 days
  905. var NGSPa1CStatisticValue_7: Decimal = 0.0
  906. var IFCCa1CStatisticValue_7: Decimal = 0.0
  907. if nr_bgs > 0 {
  908. NGSPa1CStatisticValue_7 = ((bg_7 / conversionFactor) + 46.7) / 28.7
  909. IFCCa1CStatisticValue_7 = 10.929 * (NGSPa1CStatisticValue_7 - 2.152)
  910. }
  911. // 30 days
  912. var NGSPa1CStatisticValue_30: Decimal = 0.0
  913. var IFCCa1CStatisticValue_30: Decimal = 0.0
  914. if nr_bgs > 0 {
  915. NGSPa1CStatisticValue_30 = ((bg_30 / conversionFactor) + 46.7) / 28.7
  916. IFCCa1CStatisticValue_30 = 10.929 * (NGSPa1CStatisticValue_30 - 2.152)
  917. }
  918. // Total days
  919. var NGSPa1CStatisticValue_total: Decimal = 0.0
  920. var IFCCa1CStatisticValue_total: Decimal = 0.0
  921. if nr_bgs > 0 {
  922. NGSPa1CStatisticValue_total = ((bg_total / conversionFactor) + 46.7) / 28.7
  923. IFCCa1CStatisticValue_total = 10.929 *
  924. (NGSPa1CStatisticValue_total - 2.152)
  925. }
  926. let median = Durations(
  927. day: roundDecimal(Decimal(medianCalculation(array: bgArray_1_)), 1),
  928. week: roundDecimal(Decimal(medianCalculation(array: bgArray_7_)), 1),
  929. month: roundDecimal(Decimal(medianCalculation(array: bgArray_30_)), 1),
  930. total: roundDecimal(Decimal(medianBG), 1)
  931. )
  932. // MARK: Save to Median to CoreData
  933. let saveMedianToCoreData = BGmedian(context: self.coredataContext)
  934. saveMedianToCoreData.date = Date()
  935. saveMedianToCoreData.median = median.total as NSDecimalNumber
  936. saveMedianToCoreData.median_1 = median.day as NSDecimalNumber
  937. saveMedianToCoreData.median_7 = median.week as NSDecimalNumber
  938. saveMedianToCoreData.median_30 = median.month as NSDecimalNumber
  939. try? self.coredataContext.save()
  940. var hbs = Durations(
  941. day: roundDecimal(NGSPa1CStatisticValue, 1),
  942. week: roundDecimal(NGSPa1CStatisticValue_7, 1),
  943. month: roundDecimal(NGSPa1CStatisticValue_30, 1),
  944. total: roundDecimal(NGSPa1CStatisticValue_total, 1)
  945. )
  946. let saveHbA1c = HbA1c(context: self.coredataContext)
  947. saveHbA1c.date = Date()
  948. saveHbA1c.hba1c = NGSPa1CStatisticValue_total as NSDecimalNumber
  949. saveHbA1c.hba1c_1 = NGSPa1CStatisticValue as NSDecimalNumber
  950. saveHbA1c.hba1c_7 = NGSPa1CStatisticValue_7 as NSDecimalNumber
  951. saveHbA1c.hba1c_30 = NGSPa1CStatisticValue_30 as NSDecimalNumber
  952. try? self.coredataContext.save()
  953. // Convert to user-preferred unit
  954. let overrideHbA1cUnit = settingsManager.settings.overrideHbA1cUnit
  955. if units == .mmolL {
  956. // Override if users sets overrideHbA1cUnit: true
  957. if !overrideHbA1cUnit {
  958. hbs = Durations(
  959. day: roundDecimal(IFCCa1CStatisticValue, 1),
  960. week: roundDecimal(IFCCa1CStatisticValue_7, 1),
  961. month: roundDecimal(IFCCa1CStatisticValue_30, 1),
  962. total: roundDecimal(IFCCa1CStatisticValue_total, 1)
  963. )
  964. }
  965. } else if units != .mmolL, overrideHbA1cUnit {
  966. hbs = Durations(
  967. day: roundDecimal(IFCCa1CStatisticValue, 1),
  968. week: roundDecimal(IFCCa1CStatisticValue_7, 1),
  969. month: roundDecimal(IFCCa1CStatisticValue_30, 1),
  970. total: roundDecimal(IFCCa1CStatisticValue_total, 1)
  971. )
  972. }
  973. let nrOfCGMReadings = nr1
  974. let loopstat = LoopCycles(
  975. loops: successNR + errorNR,
  976. errors: errorNR,
  977. readings: Int(nrOfCGMReadings),
  978. success_rate: Decimal(round(successRate ?? 0)),
  979. avg_interval: roundDecimal(Decimal(averageLoopTime), 1),
  980. median_interval: roundDecimal(Decimal(medianInterval), 1),
  981. min_interval: roundDecimal(Decimal(minimumInt), 1),
  982. max_interval: roundDecimal(Decimal(maximumInt), 1),
  983. avg_duration: Decimal(roundDouble(averageLoopDuration, 2)),
  984. median_duration: Decimal(roundDouble(medianLoopTime, 2)),
  985. min_duration: roundDecimal(Decimal(minimumLoopTime), 2),
  986. max_duration: Decimal(roundDouble(maximumLoopTime, 1))
  987. )
  988. // TIR calcs for every case
  989. var oneDay_: (TIR: Double, hypos: Double, hypers: Double) = (0.0, 0.0, 0.0)
  990. var sevenDays_: (TIR: Double, hypos: Double, hypers: Double) = (0.0, 0.0, 0.0)
  991. var thirtyDays_: (TIR: Double, hypos: Double, hypers: Double) = (0.0, 0.0, 0.0)
  992. var totalDays_: (TIR: Double, hypos: Double, hypers: Double) = (0.0, 0.0, 0.0)
  993. // Get all TIR calcs for every case
  994. if nr_bgs > 0 {
  995. oneDay_ = tir(bgArray_1)
  996. sevenDays_ = tir(bgArray_7)
  997. thirtyDays_ = tir(bgArray_30)
  998. totalDays_ = tir(bgArrayForTIR)
  999. }
  1000. let tir = Durations(
  1001. day: roundDecimal(Decimal(oneDay_.TIR), 1),
  1002. week: roundDecimal(Decimal(sevenDays_.TIR), 1),
  1003. month: roundDecimal(Decimal(thirtyDays_.TIR), 1),
  1004. total: roundDecimal(Decimal(totalDays_.TIR), 1)
  1005. )
  1006. let hypo = Durations(
  1007. day: Decimal(oneDay_.hypos),
  1008. week: Decimal(sevenDays_.hypos),
  1009. month: Decimal(thirtyDays_.hypos),
  1010. total: Decimal(totalDays_.hypos)
  1011. )
  1012. let hyper = Durations(
  1013. day: Decimal(oneDay_.hypers),
  1014. week: Decimal(sevenDays_.hypers),
  1015. month: Decimal(thirtyDays_.hypers),
  1016. total: Decimal(totalDays_.hypers)
  1017. )
  1018. let range = Threshold(
  1019. low: units == .mmolL ? roundDecimal(settingsManager.settings.low.asMmolL, 1) :
  1020. roundDecimal(settingsManager.settings.low, 0),
  1021. high: units == .mmolL ? roundDecimal(settingsManager.settings.high.asMmolL, 1) :
  1022. roundDecimal(settingsManager.settings.high, 0)
  1023. )
  1024. let TimeInRange = TIRs(
  1025. TIR: tir,
  1026. Hypos: hypo,
  1027. Hypers: hyper,
  1028. Threshold: range
  1029. )
  1030. let avgs = Durations(
  1031. day: roundDecimal(bg_1, 1),
  1032. week: roundDecimal(bg_7, 1),
  1033. month: roundDecimal(bg_30, 1),
  1034. total: roundDecimal(bg_total, 1)
  1035. )
  1036. let saveAverages = BGaverages(context: self.coredataContext)
  1037. saveAverages.date = Date()
  1038. saveAverages.average = bg_total as NSDecimalNumber
  1039. saveAverages.average_1 = bg_1 as NSDecimalNumber
  1040. saveAverages.average_7 = bg_7 as NSDecimalNumber
  1041. saveAverages.average_30 = bg_30 as NSDecimalNumber
  1042. try? self.coredataContext.save()
  1043. let avg = Averages(Average: avgs, Median: median)
  1044. var insulinDistribution = [InsulinDistribution]()
  1045. var insulin = Ins(
  1046. TDD: 0,
  1047. bolus: 0,
  1048. temp_basal: 0,
  1049. scheduled_basal: 0,
  1050. total_average: 0
  1051. )
  1052. let requestInsulinDistribution = InsulinDistribution.fetchRequest() as NSFetchRequest<InsulinDistribution>
  1053. let sortInsulin = NSSortDescriptor(key: "date", ascending: false)
  1054. requestInsulinDistribution.sortDescriptors = [sortInsulin]
  1055. try? insulinDistribution = coredataContext.fetch(requestInsulinDistribution)
  1056. insulin = Ins(
  1057. TDD: roundDecimal(currentTDD, 2),
  1058. bolus: insulinDistribution.first != nil ? ((insulinDistribution.first?.bolus ?? 0) as Decimal) : 0,
  1059. temp_basal: insulinDistribution.first != nil ? ((insulinDistribution.first?.tempBasal ?? 0) as Decimal) : 0,
  1060. scheduled_basal: insulinDistribution
  1061. .first != nil ? ((insulinDistribution.first?.scheduledBasal ?? 0) as Decimal) : 0,
  1062. total_average: roundDecimal(tddTotalAverage, 1)
  1063. )
  1064. var sumOfSquares = 0.0
  1065. var sumOfSquares_1 = 0.0
  1066. var sumOfSquares_7 = 0.0
  1067. var sumOfSquares_30 = 0.0
  1068. // Total
  1069. for array in bgArray {
  1070. sumOfSquares += pow(array - Double(bg_total), 2)
  1071. }
  1072. // One day
  1073. for array_1 in bgArray_1_ {
  1074. sumOfSquares_1 += pow(array_1 - Double(bg_1), 2)
  1075. }
  1076. // week
  1077. for array_7 in bgArray_7_ {
  1078. sumOfSquares_7 += pow(array_7 - Double(bg_7), 2)
  1079. }
  1080. // month
  1081. for array_30 in bgArray_30_ {
  1082. sumOfSquares_30 += pow(array_30 - Double(bg_30), 2)
  1083. }
  1084. // Standard deviation and Coefficient of variation
  1085. var sd_total = 0.0
  1086. var cv_total = 0.0
  1087. var sd_1 = 0.0
  1088. var cv_1 = 0.0
  1089. var sd_7 = 0.0
  1090. var cv_7 = 0.0
  1091. var sd_30 = 0.0
  1092. var cv_30 = 0.0
  1093. // Avoid division by zero
  1094. if bg_total > 0 {
  1095. sd_total = sqrt(sumOfSquares / Double(nr_bgs))
  1096. cv_total = sd_total / Double(bg_total) * 100
  1097. }
  1098. if bg_1 > 0 {
  1099. sd_1 = sqrt(sumOfSquares_1 / Double(bgArray_1_.count))
  1100. cv_1 = sd_1 / Double(bg_1) * 100
  1101. }
  1102. if bg_7 > 0 {
  1103. sd_7 = sqrt(sumOfSquares_7 / Double(bgArray_7_.count))
  1104. cv_7 = sd_7 / Double(bg_7) * 100
  1105. }
  1106. if bg_30 > 0 {
  1107. sd_30 = sqrt(sumOfSquares_30 / Double(bgArray_30_.count))
  1108. cv_30 = sd_30 / Double(bg_30) * 100
  1109. }
  1110. // Standard Deviations
  1111. let standardDeviations = Durations(
  1112. day: roundDecimal(Decimal(sd_1), 1),
  1113. week: roundDecimal(Decimal(sd_7), 1),
  1114. month: roundDecimal(Decimal(sd_30), 1),
  1115. total: roundDecimal(Decimal(sd_total), 1)
  1116. )
  1117. // CV = standard deviation / sample mean x 100
  1118. let cvs = Durations(
  1119. day: roundDecimal(Decimal(cv_1), 1),
  1120. week: roundDecimal(Decimal(cv_7), 1),
  1121. month: roundDecimal(Decimal(cv_30), 1),
  1122. total: roundDecimal(Decimal(cv_total), 1)
  1123. )
  1124. let variance = Variance(SD: standardDeviations, CV: cvs)
  1125. let dailystat = Statistics(
  1126. created_at: Date(),
  1127. iPhone: UIDevice.current.getDeviceId,
  1128. iOS: UIDevice.current.getOSInfo,
  1129. Build_Version: version ?? "",
  1130. Build_Number: build ?? "1",
  1131. Branch: branch,
  1132. CopyRightNotice: String(copyrightNotice_.prefix(32)),
  1133. Build_Date: buildDate,
  1134. Algorithm: algo_,
  1135. AdjustmentFactor: af,
  1136. Pump: pump_,
  1137. CGM: cgm.rawValue,
  1138. insulinType: insulin_type.rawValue,
  1139. peakActivityTime: iPa,
  1140. Carbs_24h: carbTotal,
  1141. GlucoseStorage_Days: Decimal(roundDouble(numberOfDays, 1)),
  1142. Statistics: Stats(
  1143. Distribution: TimeInRange,
  1144. Glucose: avg,
  1145. HbA1c: hbs,
  1146. LoopCycles: loopstat,
  1147. Insulin: insulin,
  1148. Variance: variance
  1149. )
  1150. )
  1151. storage.save(dailystat, as: file)
  1152. nightscout.uploadStatistics(dailystat: dailystat)
  1153. nightscout.uploadPreferences()
  1154. let saveStatsCoreData = StatsData(context: self.coredataContext)
  1155. saveStatsCoreData.lastrun = Date()
  1156. try? self.coredataContext.save()
  1157. print("Test time of statistics computation: \(-1 * now.timeIntervalSinceNow) s")
  1158. }
  1159. }
  1160. }
  1161. private func loopStats(loopStatRecord: LoopStats) {
  1162. let LoopStatsStartedAt = Date()
  1163. coredataContext.perform {
  1164. let nLS = LoopStatRecord(context: self.coredataContext)
  1165. nLS.start = loopStatRecord.start
  1166. nLS.end = loopStatRecord.end ?? Date()
  1167. nLS.loopStatus = loopStatRecord.loopStatus
  1168. nLS.duration = loopStatRecord.duration ?? 0.0
  1169. try? self.coredataContext.save()
  1170. }
  1171. print("Test time of LoopStats computation: \(-1 * LoopStatsStartedAt.timeIntervalSinceNow) s")
  1172. }
  1173. private func processError(_ error: Error) {
  1174. warning(.apsManager, "\(error.localizedDescription)")
  1175. lastError.send(error)
  1176. }
  1177. private func createBolusReporter() {
  1178. bolusReporter = pumpManager?.createBolusProgressReporter(reportingOn: processQueue)
  1179. bolusReporter?.addObserver(self)
  1180. }
  1181. private func updateStatus() {
  1182. debug(.apsManager, "force update status")
  1183. guard let pump = pumpManager else {
  1184. return
  1185. }
  1186. if let omnipod = pump as? OmnipodPumpManager {
  1187. omnipod.getPodStatus { _ in }
  1188. }
  1189. if let omnipodBLE = pump as? OmniBLEPumpManager {
  1190. omnipodBLE.getPodStatus { _ in }
  1191. }
  1192. }
  1193. private func clearBolusReporter() {
  1194. bolusReporter?.removeObserver(self)
  1195. bolusReporter = nil
  1196. processQueue.asyncAfter(deadline: .now() + 0.5) {
  1197. self.bolusProgress.send(nil)
  1198. self.updateStatus()
  1199. }
  1200. }
  1201. }
  1202. private extension PumpManager {
  1203. func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval) -> AnyPublisher<DoseEntry?, Error> {
  1204. Future { promise in
  1205. self.enactTempBasal(unitsPerHour: unitsPerHour, for: duration) { error in
  1206. if let error = error {
  1207. debug(.apsManager, "Temp basal failed: \(unitsPerHour) for: \(duration)")
  1208. promise(.failure(error))
  1209. } else {
  1210. debug(.apsManager, "Temp basal succeded: \(unitsPerHour) for: \(duration)")
  1211. promise(.success(nil))
  1212. }
  1213. }
  1214. }
  1215. .mapError { APSError.pumpError($0) }
  1216. .eraseToAnyPublisher()
  1217. }
  1218. func enactBolus(units: Double, automatic: Bool) -> AnyPublisher<DoseEntry?, Error> {
  1219. Future { promise in
  1220. // convert automatic
  1221. let automaticValue = automatic ? BolusActivationType.automatic : BolusActivationType.manualRecommendationAccepted
  1222. self.enactBolus(units: units, activationType: automaticValue) { error in
  1223. if let error = error {
  1224. debug(.apsManager, "Bolus failed: \(units)")
  1225. promise(.failure(error))
  1226. } else {
  1227. debug(.apsManager, "Bolus succeded: \(units)")
  1228. promise(.success(nil))
  1229. }
  1230. }
  1231. }
  1232. .mapError { APSError.pumpError($0) }
  1233. .eraseToAnyPublisher()
  1234. }
  1235. func cancelBolus() -> AnyPublisher<DoseEntry?, Error> {
  1236. Future { promise in
  1237. self.cancelBolus { result in
  1238. switch result {
  1239. case let .success(dose):
  1240. debug(.apsManager, "Cancel Bolus succeded")
  1241. promise(.success(dose))
  1242. case let .failure(error):
  1243. debug(.apsManager, "Cancel Bolus failed")
  1244. promise(.failure(error))
  1245. }
  1246. }
  1247. }
  1248. .mapError { APSError.pumpError($0) }
  1249. .eraseToAnyPublisher()
  1250. }
  1251. func suspendDelivery() -> AnyPublisher<Void, Error> {
  1252. Future { promise in
  1253. self.suspendDelivery { error in
  1254. if let error = error {
  1255. promise(.failure(error))
  1256. } else {
  1257. promise(.success(()))
  1258. }
  1259. }
  1260. }
  1261. .mapError { APSError.pumpError($0) }
  1262. .eraseToAnyPublisher()
  1263. }
  1264. func resumeDelivery() -> AnyPublisher<Void, Error> {
  1265. Future { promise in
  1266. self.resumeDelivery { error in
  1267. if let error = error {
  1268. promise(.failure(error))
  1269. } else {
  1270. promise(.success(()))
  1271. }
  1272. }
  1273. }
  1274. .mapError { APSError.pumpError($0) }
  1275. .eraseToAnyPublisher()
  1276. }
  1277. }
  1278. extension BaseAPSManager: PumpManagerStatusObserver {
  1279. func pumpManager(_: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) {
  1280. let percent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100)
  1281. let battery = Battery(
  1282. percent: percent,
  1283. voltage: nil,
  1284. string: percent > 10 ? .normal : .low,
  1285. display: status.pumpBatteryChargeRemaining != nil
  1286. )
  1287. storage.save(battery, as: OpenAPS.Monitor.battery)
  1288. storage.save(status.pumpStatus, as: OpenAPS.Monitor.status)
  1289. }
  1290. }
  1291. extension BaseAPSManager: DoseProgressObserver {
  1292. func doseProgressReporterDidUpdate(_ doseProgressReporter: DoseProgressReporter) {
  1293. bolusProgress.send(Decimal(doseProgressReporter.progress.percentComplete))
  1294. if doseProgressReporter.progress.isComplete {
  1295. clearBolusReporter()
  1296. }
  1297. }
  1298. }
  1299. extension PumpManagerStatus {
  1300. var pumpStatus: PumpStatus {
  1301. let bolusing = bolusState != .noBolus
  1302. let suspended = basalDeliveryState?.isSuspended ?? true
  1303. let type = suspended ? StatusType.suspended : (bolusing ? .bolusing : .normal)
  1304. return PumpStatus(status: type, bolusing: bolusing, suspended: suspended, timestamp: Date())
  1305. }
  1306. }