APSManager.swift 56 KB

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