APSManager.swift 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401
  1. import Combine
  2. import Foundation
  3. import LoopKit
  4. import LoopKitUI
  5. import OmniBLE
  6. import OmniKit
  7. import RileyLinkKit
  8. import SwiftDate
  9. import Swinject
  10. protocol APSManager {
  11. func heartbeat(date: Date)
  12. func autotune() -> AnyPublisher<Autotune?, Never>
  13. func enactBolus(amount: Double, isSMB: Bool)
  14. var pumpManager: PumpManagerUI? { get set }
  15. var bluetoothManager: BluetoothStateManager? { get }
  16. var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
  17. var pumpName: CurrentValueSubject<String, Never> { get }
  18. var isLooping: CurrentValueSubject<Bool, Never> { get }
  19. var lastLoopDate: Date { get }
  20. var lastLoopDateSubject: PassthroughSubject<Date, Never> { get }
  21. var bolusProgress: CurrentValueSubject<Decimal?, Never> { get }
  22. var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
  23. var isManualTempBasal: Bool { get }
  24. func enactTempBasal(rate: Double, duration: TimeInterval)
  25. func makeProfiles() -> AnyPublisher<Bool, Never>
  26. func determineBasal() -> AnyPublisher<Bool, Never>
  27. func determineBasalSync()
  28. func roundBolus(amount: Decimal) -> Decimal
  29. var lastError: CurrentValueSubject<Error?, Never> { get }
  30. func cancelBolus()
  31. func enactAnnouncement(_ announcement: Announcement)
  32. }
  33. enum APSError: LocalizedError {
  34. case pumpError(Error)
  35. case invalidPumpState(message: String)
  36. case glucoseError(message: String)
  37. case apsError(message: String)
  38. case deviceSyncError(message: String)
  39. case manualBasalTemp(message: String)
  40. var errorDescription: String? {
  41. switch self {
  42. case let .pumpError(error):
  43. return "Pump error: \(error.localizedDescription)"
  44. case let .invalidPumpState(message):
  45. return "Error: Invalid Pump State: \(message)"
  46. case let .glucoseError(message):
  47. return "Error: Invalid glucose: \(message)"
  48. case let .apsError(message):
  49. return "APS error: \(message)"
  50. case let .deviceSyncError(message):
  51. return "Sync error: \(message)"
  52. case let .manualBasalTemp(message):
  53. return "Manual Basal Temp : \(message)"
  54. }
  55. }
  56. }
  57. final class BaseAPSManager: APSManager, Injectable {
  58. private let processQueue = DispatchQueue(label: "BaseAPSManager.processQueue")
  59. @Injected() private var storage: FileStorage!
  60. @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
  61. @Injected() private var alertHistoryStorage: AlertHistoryStorage!
  62. @Injected() private var glucoseStorage: GlucoseStorage!
  63. @Injected() private var tempTargetsStorage: TempTargetsStorage!
  64. @Injected() private var carbsStorage: CarbsStorage!
  65. @Injected() private var announcementsStorage: AnnouncementsStorage!
  66. @Injected() private var deviceDataManager: DeviceDataManager!
  67. @Injected() private var nightscout: NightscoutManager!
  68. @Injected() private var settingsManager: SettingsManager!
  69. @Injected() private var broadcaster: Broadcaster!
  70. @Persisted(key: "lastAutotuneDate") private var lastAutotuneDate = Date()
  71. @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
  72. didSet {
  73. lastLoopDateSubject.send(lastLoopDate)
  74. }
  75. }
  76. private var openAPS: OpenAPS!
  77. private var lifetime = Lifetime()
  78. var pumpManager: PumpManagerUI? {
  79. get { deviceDataManager.pumpManager }
  80. set { deviceDataManager.pumpManager = newValue }
  81. }
  82. var bluetoothManager: BluetoothStateManager? { deviceDataManager.bluetoothManager }
  83. @Persisted(key: "isManualTempBasal") var isManualTempBasal: Bool = false
  84. let isLooping = CurrentValueSubject<Bool, Never>(false)
  85. let lastLoopDateSubject = PassthroughSubject<Date, Never>()
  86. let lastError = CurrentValueSubject<Error?, Never>(nil)
  87. let bolusProgress = CurrentValueSubject<Decimal?, Never>(nil)
  88. var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> {
  89. deviceDataManager.pumpDisplayState
  90. }
  91. var pumpName: CurrentValueSubject<String, Never> {
  92. deviceDataManager.pumpName
  93. }
  94. var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> {
  95. deviceDataManager.pumpExpiresAtDate
  96. }
  97. var settings: FreeAPSSettings {
  98. get { settingsManager.settings }
  99. set { settingsManager.settings = newValue }
  100. }
  101. init(resolver: Resolver) {
  102. injectServices(resolver)
  103. openAPS = OpenAPS(storage: storage)
  104. subscribe()
  105. lastLoopDateSubject.send(lastLoopDate)
  106. isLooping
  107. .weakAssign(to: \.deviceDataManager.loopInProgress, on: self)
  108. .store(in: &lifetime)
  109. }
  110. private func subscribe() {
  111. deviceDataManager.recommendsLoop
  112. .receive(on: processQueue)
  113. .sink { [weak self] in
  114. self?.loop()
  115. }
  116. .store(in: &lifetime)
  117. pumpManager?.addStatusObserver(self, queue: processQueue)
  118. deviceDataManager.errorSubject
  119. .receive(on: processQueue)
  120. .map { APSError.pumpError($0) }
  121. .sink {
  122. self.processError($0)
  123. }
  124. .store(in: &lifetime)
  125. deviceDataManager.bolusTrigger
  126. .receive(on: processQueue)
  127. .sink { bolusing in
  128. if bolusing {
  129. self.createBolusReporter()
  130. } else {
  131. self.clearBolusReporter()
  132. }
  133. }
  134. .store(in: &lifetime)
  135. // manage a manual Temp Basal from OmniPod - Force loop() after stop a temp basal or finished
  136. deviceDataManager.manualTempBasal
  137. .receive(on: processQueue)
  138. .sink { manualBasal in
  139. if manualBasal {
  140. self.isManualTempBasal = true
  141. } else {
  142. if self.isManualTempBasal {
  143. self.isManualTempBasal = false
  144. self.loop()
  145. }
  146. }
  147. }
  148. .store(in: &lifetime)
  149. }
  150. func heartbeat(date: Date) {
  151. deviceDataManager.heartbeat(date: date)
  152. }
  153. // Loop entry point
  154. private func loop() {
  155. guard !isLooping.value else {
  156. warning(.apsManager, "Already looping, skip")
  157. return
  158. }
  159. loopStats(starting: true)
  160. debug(.apsManager, "Starting loop")
  161. isLooping.send(true)
  162. determineBasal()
  163. .replaceEmpty(with: false)
  164. .flatMap { [weak self] success -> AnyPublisher<Void, Error> in
  165. guard let self = self, success else {
  166. return Fail(error: APSError.apsError(message: "Determine basal failed")).eraseToAnyPublisher()
  167. }
  168. // Open loop completed
  169. guard self.settings.closedLoop else {
  170. return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
  171. }
  172. self.nightscout.uploadStatus()
  173. // Closed loop - enact suggested
  174. return self.enactSuggested()
  175. }
  176. .sink { [weak self] completion in
  177. guard let self = self else { return }
  178. if case let .failure(error) = completion {
  179. self.loopCompleted(error: error)
  180. } else {
  181. self.loopCompleted()
  182. }
  183. } receiveValue: {}
  184. .store(in: &lifetime)
  185. }
  186. // Loop exit point
  187. private func loopCompleted(error: Error? = nil) {
  188. isLooping.send(false)
  189. if let error = error {
  190. loopStats(error: error, starting: false)
  191. warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
  192. processError(error)
  193. } else {
  194. loopStats(starting: false)
  195. debug(.apsManager, "Loop succeeded")
  196. lastLoopDate = Date()
  197. lastError.send(nil)
  198. }
  199. if settings.closedLoop {
  200. reportEnacted(received: error == nil)
  201. }
  202. }
  203. private func verifyStatus() -> Error? {
  204. guard let pump = pumpManager else {
  205. return APSError.invalidPumpState(message: "Pump not set")
  206. }
  207. let status = pump.status.pumpStatus
  208. guard !status.bolusing else {
  209. return APSError.invalidPumpState(message: "Pump is bolusing")
  210. }
  211. guard !status.suspended else {
  212. return APSError.invalidPumpState(message: "Pump suspended")
  213. }
  214. let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
  215. guard reservoir >= 0 else {
  216. return APSError.invalidPumpState(message: "Reservoir is empty")
  217. }
  218. return nil
  219. }
  220. private func autosens() -> AnyPublisher<Bool, Never> {
  221. guard let autosens = storage.retrieve(OpenAPS.Settings.autosense, as: Autosens.self),
  222. (autosens.timestamp ?? .distantPast).addingTimeInterval(30.minutes.timeInterval) > Date()
  223. else {
  224. return openAPS.autosense()
  225. .map { $0 != nil }
  226. .eraseToAnyPublisher()
  227. }
  228. return Just(false).eraseToAnyPublisher()
  229. }
  230. func determineBasal() -> AnyPublisher<Bool, Never> {
  231. debug(.apsManager, "Start determine basal")
  232. guard let glucose = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.isNotEmpty else {
  233. debug(.apsManager, "Not enough glucose data")
  234. processError(APSError.glucoseError(message: "Not enough glucose data"))
  235. return Just(false).eraseToAnyPublisher()
  236. }
  237. let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
  238. guard lastGlucoseDate >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
  239. debug(.apsManager, "Glucose data is stale")
  240. processError(APSError.glucoseError(message: "Glucose data is stale"))
  241. return Just(false).eraseToAnyPublisher()
  242. }
  243. guard glucoseStorage.isGlucoseNotFlat() else {
  244. debug(.apsManager, "Glucose data is too flat")
  245. processError(APSError.glucoseError(message: "Glucose data is too flat"))
  246. return Just(false).eraseToAnyPublisher()
  247. }
  248. let now = Date()
  249. let temp = currentTemp(date: now)
  250. let mainPublisher = makeProfiles()
  251. .flatMap { _ in self.autosens() }
  252. .flatMap { _ in self.dailyAutotune() }
  253. .flatMap { _ in self.openAPS.determineBasal(currentTemp: temp, clock: now) }
  254. .map { suggestion -> Bool in
  255. if let suggestion = suggestion {
  256. DispatchQueue.main.async {
  257. self.broadcaster.notify(SuggestionObserver.self, on: .main) {
  258. $0.suggestionDidUpdate(suggestion)
  259. }
  260. }
  261. }
  262. return suggestion != nil
  263. }
  264. .eraseToAnyPublisher()
  265. if temp.duration == 0,
  266. settings.closedLoop,
  267. settingsManager.preferences.unsuspendIfNoTemp,
  268. let pump = pumpManager,
  269. pump.status.pumpStatus.suspended
  270. {
  271. return pump.resumeDelivery()
  272. .flatMap { _ in mainPublisher }
  273. .replaceError(with: false)
  274. .eraseToAnyPublisher()
  275. }
  276. return mainPublisher
  277. }
  278. func determineBasalSync() {
  279. determineBasal().cancellable().store(in: &lifetime)
  280. }
  281. func makeProfiles() -> AnyPublisher<Bool, Never> {
  282. openAPS.makeProfiles(useAutotune: settings.useAutotune)
  283. .map { tunedProfile in
  284. if let basalProfile = tunedProfile?.basalProfile {
  285. self.processQueue.async {
  286. self.broadcaster.notify(BasalProfileObserver.self, on: self.processQueue) {
  287. $0.basalProfileDidChange(basalProfile)
  288. }
  289. }
  290. }
  291. return tunedProfile != nil
  292. }
  293. .eraseToAnyPublisher()
  294. }
  295. func roundBolus(amount: Decimal) -> Decimal {
  296. guard let pump = pumpManager else { return amount }
  297. let rounded = Decimal(pump.roundToSupportedBolusVolume(units: Double(amount)))
  298. let maxBolus = Decimal(pump.roundToSupportedBolusVolume(units: Double(settingsManager.pumpSettings.maxBolus)))
  299. return min(rounded, maxBolus)
  300. }
  301. private var bolusReporter: DoseProgressReporter?
  302. func enactBolus(amount: Double, isSMB: Bool) {
  303. if let error = verifyStatus() {
  304. processError(error)
  305. processQueue.async {
  306. self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
  307. $0.bolusDidFail()
  308. }
  309. }
  310. return
  311. }
  312. guard let pump = pumpManager else { return }
  313. let roundedAmout = pump.roundToSupportedBolusVolume(units: amount)
  314. debug(.apsManager, "Enact bolus \(roundedAmout), manual \(!isSMB)")
  315. pump.enactBolus(units: roundedAmout, automatic: isSMB).sink { completion in
  316. if case let .failure(error) = completion {
  317. warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
  318. self.processError(APSError.pumpError(error))
  319. if !isSMB {
  320. self.processQueue.async {
  321. self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
  322. $0.bolusDidFail()
  323. }
  324. }
  325. }
  326. } else {
  327. debug(.apsManager, "Bolus succeeded")
  328. if !isSMB {
  329. self.determineBasal().sink { _ in }.store(in: &self.lifetime)
  330. }
  331. self.bolusProgress.send(0)
  332. }
  333. } receiveValue: { _ in }
  334. .store(in: &lifetime)
  335. }
  336. func cancelBolus() {
  337. guard let pump = pumpManager, pump.status.pumpStatus.bolusing else { return }
  338. debug(.apsManager, "Cancel bolus")
  339. pump.cancelBolus().sink { completion in
  340. if case let .failure(error) = completion {
  341. debug(.apsManager, "Bolus cancellation failed with error: \(error.localizedDescription)")
  342. self.processError(APSError.pumpError(error))
  343. } else {
  344. debug(.apsManager, "Bolus cancelled")
  345. }
  346. self.bolusReporter?.removeObserver(self)
  347. self.bolusReporter = nil
  348. self.bolusProgress.send(nil)
  349. } receiveValue: { _ in }
  350. .store(in: &lifetime)
  351. }
  352. func enactTempBasal(rate: Double, duration: TimeInterval) {
  353. if let error = verifyStatus() {
  354. processError(error)
  355. return
  356. }
  357. guard let pump = pumpManager else { return }
  358. // unable to do temp basal during manual temp basal 😁
  359. if isManualTempBasal {
  360. processError(APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  361. return
  362. }
  363. debug(.apsManager, "Enact temp basal \(rate) - \(duration)")
  364. let roundedAmout = pump.roundToSupportedBasalRate(unitsPerHour: rate)
  365. pump.enactTempBasal(unitsPerHour: roundedAmout, for: duration) { error in
  366. if let error = error {
  367. debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
  368. self.processError(APSError.pumpError(error))
  369. } else {
  370. debug(.apsManager, "Temp Basal succeeded")
  371. let temp = TempBasal(duration: Int(duration / 60), rate: Decimal(rate), temp: .absolute, timestamp: Date())
  372. self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
  373. if rate == 0, duration == 0 {
  374. self.pumpHistoryStorage.saveCancelTempEvents()
  375. }
  376. }
  377. }
  378. }
  379. func dailyAutotune() -> AnyPublisher<Bool, Never> {
  380. guard settings.useAutotune else {
  381. return Just(false).eraseToAnyPublisher()
  382. }
  383. let now = Date()
  384. guard lastAutotuneDate.isBeforeDate(now, granularity: .day) else {
  385. return Just(false).eraseToAnyPublisher()
  386. }
  387. lastAutotuneDate = now
  388. return autotune().map { $0 != nil }.eraseToAnyPublisher()
  389. }
  390. func autotune() -> AnyPublisher<Autotune?, Never> {
  391. openAPS.autotune().eraseToAnyPublisher()
  392. }
  393. func enactAnnouncement(_ announcement: Announcement) {
  394. guard let action = announcement.action else {
  395. warning(.apsManager, "Invalid Announcement action")
  396. return
  397. }
  398. guard let pump = pumpManager else {
  399. warning(.apsManager, "Pump is not set")
  400. return
  401. }
  402. debug(.apsManager, "Start enact announcement: \(action)")
  403. switch action {
  404. case let .bolus(amount):
  405. if let error = verifyStatus() {
  406. processError(error)
  407. return
  408. }
  409. let roundedAmount = pump.roundToSupportedBolusVolume(units: Double(amount))
  410. pump.enactBolus(units: roundedAmount, activationType: .manualRecommendationAccepted) { error in
  411. if let error = error {
  412. // warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
  413. switch error {
  414. case .uncertainDelivery:
  415. // Do not generate notification on uncertain delivery error
  416. break
  417. default:
  418. // Do not generate notifications for automatic boluses that fail.
  419. warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
  420. }
  421. } else {
  422. debug(.apsManager, "Announcement Bolus succeeded")
  423. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  424. self.bolusProgress.send(0)
  425. }
  426. }
  427. case let .pump(pumpAction):
  428. switch pumpAction {
  429. case .suspend:
  430. if let error = verifyStatus() {
  431. processError(error)
  432. return
  433. }
  434. pump.suspendDelivery { error in
  435. if let error = error {
  436. debug(.apsManager, "Pump not suspended by Announcement: \(error.localizedDescription)")
  437. } else {
  438. debug(.apsManager, "Pump suspended by Announcement")
  439. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  440. self.nightscout.uploadStatus()
  441. }
  442. }
  443. case .resume:
  444. guard pump.status.pumpStatus.suspended else {
  445. return
  446. }
  447. pump.resumeDelivery { error in
  448. if let error = error {
  449. warning(.apsManager, "Pump not resumed by Announcement: \(error.localizedDescription)")
  450. } else {
  451. debug(.apsManager, "Pump resumed by Announcement")
  452. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  453. self.nightscout.uploadStatus()
  454. }
  455. }
  456. }
  457. case let .looping(closedLoop):
  458. settings.closedLoop = closedLoop
  459. debug(.apsManager, "Closed loop \(closedLoop) by Announcement")
  460. announcementsStorage.storeAnnouncements([announcement], enacted: true)
  461. case let .tempbasal(rate, duration):
  462. if let error = verifyStatus() {
  463. processError(error)
  464. return
  465. }
  466. // unable to do temp basal during manual temp basal 😁
  467. if isManualTempBasal {
  468. processError(APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  469. return
  470. }
  471. guard !settings.closedLoop else {
  472. return
  473. }
  474. let roundedRate = pump.roundToSupportedBasalRate(unitsPerHour: Double(rate))
  475. pump.enactTempBasal(unitsPerHour: roundedRate, for: TimeInterval(duration) * 60) { error in
  476. if let error = error {
  477. warning(.apsManager, "Announcement TempBasal failed with error: \(error.localizedDescription)")
  478. } else {
  479. debug(.apsManager, "Announcement TempBasal succeeded")
  480. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  481. }
  482. }
  483. }
  484. }
  485. private func currentTemp(date: Date) -> TempBasal {
  486. let defaultTemp = { () -> TempBasal in
  487. guard let temp = storage.retrieve(OpenAPS.Monitor.tempBasal, as: TempBasal.self) else {
  488. return TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: Date())
  489. }
  490. let delta = Int((date.timeIntervalSince1970 - temp.timestamp.timeIntervalSince1970) / 60)
  491. let duration = max(0, temp.duration - delta)
  492. return TempBasal(duration: duration, rate: temp.rate, temp: .absolute, timestamp: date)
  493. }()
  494. guard let state = pumpManager?.status.basalDeliveryState else { return defaultTemp }
  495. switch state {
  496. case .active:
  497. return TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: date)
  498. case let .tempBasal(dose):
  499. let rate = Decimal(dose.unitsPerHour)
  500. let durationMin = max(0, Int((dose.endDate.timeIntervalSince1970 - date.timeIntervalSince1970) / 60))
  501. return TempBasal(duration: durationMin, rate: rate, temp: .absolute, timestamp: date)
  502. default:
  503. return defaultTemp
  504. }
  505. }
  506. private func enactSuggested() -> AnyPublisher<Void, Error> {
  507. guard let suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) else {
  508. return Fail(error: APSError.apsError(message: "Suggestion not found")).eraseToAnyPublisher()
  509. }
  510. guard Date().timeIntervalSince(suggested.deliverAt ?? .distantPast) < Config.eхpirationInterval else {
  511. return Fail(error: APSError.apsError(message: "Suggestion expired")).eraseToAnyPublisher()
  512. }
  513. guard let pump = pumpManager else {
  514. return Fail(error: APSError.apsError(message: "Pump not set")).eraseToAnyPublisher()
  515. }
  516. // unable to do temp basal during manual temp basal 😁
  517. if isManualTempBasal {
  518. return Fail(error: APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  519. .eraseToAnyPublisher()
  520. }
  521. let basalPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
  522. if let error = self.verifyStatus() {
  523. return Fail(error: error).eraseToAnyPublisher()
  524. }
  525. guard let rate = suggested.rate, let duration = suggested.duration else {
  526. // It is OK, no temp required
  527. debug(.apsManager, "No temp required")
  528. return Just(()).setFailureType(to: Error.self)
  529. .eraseToAnyPublisher()
  530. }
  531. return pump.enactTempBasal(unitsPerHour: Double(rate), for: TimeInterval(duration * 60)).map { _ in
  532. let temp = TempBasal(duration: duration, rate: rate, temp: .absolute, timestamp: Date())
  533. self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
  534. return ()
  535. }
  536. .eraseToAnyPublisher()
  537. }.eraseToAnyPublisher()
  538. let bolusPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
  539. if let error = self.verifyStatus() {
  540. return Fail(error: error).eraseToAnyPublisher()
  541. }
  542. guard let units = suggested.units else {
  543. // It is OK, no bolus required
  544. debug(.apsManager, "No bolus required")
  545. return Just(()).setFailureType(to: Error.self)
  546. .eraseToAnyPublisher()
  547. }
  548. return pump.enactBolus(units: Double(units), automatic: true).map { _ in
  549. self.bolusProgress.send(0)
  550. return ()
  551. }
  552. .eraseToAnyPublisher()
  553. }.eraseToAnyPublisher()
  554. return basalPublisher.flatMap { bolusPublisher }.eraseToAnyPublisher()
  555. }
  556. private func reportEnacted(received: Bool) {
  557. if let suggestion = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self), suggestion.deliverAt != nil {
  558. var enacted = suggestion
  559. enacted.timestamp = Date()
  560. enacted.recieved = received
  561. storage.save(enacted, as: OpenAPS.Enact.enacted)
  562. // Create a tdd.json
  563. tdd(enacted_: enacted)
  564. // Create a dailyStats.json
  565. dailyStats()
  566. debug(.apsManager, "Suggestion enacted. Received: \(received)")
  567. DispatchQueue.main.async {
  568. self.broadcaster.notify(EnactedSuggestionObserver.self, on: .main) {
  569. $0.enactedSuggestionDidUpdate(enacted)
  570. }
  571. }
  572. nightscout.uploadStatus()
  573. }
  574. }
  575. private func tdd(enacted_: Suggestion) {
  576. // Add to tdd.json:
  577. let preferences = settingsManager.preferences
  578. let currentTDD = enacted_.tdd ?? 0
  579. let file = OpenAPS.Monitor.tdd
  580. let tdd = TDD(
  581. TDD: currentTDD,
  582. timestamp: Date(),
  583. id: UUID().uuidString
  584. )
  585. var uniqEvents: [TDD] = []
  586. storage.transaction { storage in
  587. storage.append(tdd, to: file, uniqBy: \.id)
  588. uniqEvents = storage.retrieve(file, as: [TDD].self)?
  589. .filter { $0.timestamp.addingTimeInterval(14.days.timeInterval) > Date() }
  590. .sorted { $0.timestamp > $1.timestamp } ?? []
  591. var total: Decimal = 0
  592. var indeces: Decimal = 0
  593. for uniqEvent in uniqEvents {
  594. if uniqEvent.TDD > 0 {
  595. total += uniqEvent.TDD
  596. indeces += 1
  597. }
  598. }
  599. let entriesPast2hours = storage.retrieve(file, as: [TDD].self)?
  600. .filter { $0.timestamp.addingTimeInterval(2.hours.timeInterval) > Date() }
  601. .sorted { $0.timestamp > $1.timestamp } ?? []
  602. var totalAmount: Decimal = 0
  603. var nrOfIndeces: Decimal = 0
  604. for entry in entriesPast2hours {
  605. if entry.TDD > 0 {
  606. totalAmount += entry.TDD
  607. nrOfIndeces += 1
  608. }
  609. }
  610. if indeces == 0 {
  611. indeces = 1
  612. }
  613. if nrOfIndeces == 0 {
  614. nrOfIndeces = 1
  615. }
  616. let average14 = total / indeces
  617. let average2hours = totalAmount / nrOfIndeces
  618. let weight = preferences.weightPercentage
  619. let weighted_average = weight * average2hours + (1 - weight) * average14
  620. let averages = TDD_averages(
  621. average_total_data: roundDecimal(average14, 1),
  622. weightedAverage: roundDecimal(weighted_average, 1),
  623. past2hoursAverage: roundDecimal(average2hours, 1),
  624. date: Date()
  625. )
  626. storage.save(averages, as: OpenAPS.Monitor.tdd_averages)
  627. storage.save(Array(uniqEvents), as: file)
  628. }
  629. }
  630. private func roundDecimal(_ decimal: Decimal, _ digits: Double) -> Decimal {
  631. let rounded = round(Double(decimal) * pow(10, digits)) / pow(10, digits)
  632. return Decimal(rounded)
  633. }
  634. private func roundDouble(_ double: Double, _ digits: Double) -> Double {
  635. let rounded = round(Double(double) * pow(10, digits)) / pow(10, digits)
  636. return rounded
  637. }
  638. private func medianCalculation(array: [Double]) -> Double {
  639. guard !array.isEmpty else {
  640. return 0
  641. }
  642. let sorted = array.sorted()
  643. let length = array.count
  644. if length % 2 == 0 {
  645. return (sorted[length / 2 - 1] + sorted[length / 2]) / 2.0
  646. }
  647. return Double(sorted[length / 2])
  648. }
  649. // Add to dailyStats.JSON
  650. private func dailyStats() {
  651. var testFile: [DailyStats] = []
  652. var testIfEmpty = 0
  653. storage.transaction { storage in
  654. testFile = storage.retrieve(OpenAPS.Monitor.dailyStats, as: [DailyStats].self) ?? []
  655. testIfEmpty = testFile.count
  656. }
  657. // Only run every hour
  658. if testIfEmpty != 0 {
  659. guard testFile[0].createdAt.addingTimeInterval(1.hours.timeInterval) < Date() else {
  660. return
  661. }
  662. }
  663. let preferences = settingsManager.preferences
  664. let carbs = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)
  665. let tdds = storage.retrieve(OpenAPS.Monitor.tdd, as: [TDD].self)
  666. var currentTDD: Decimal = 0
  667. if tdds?.count ?? 0 > 0 {
  668. currentTDD = tdds?[0].TDD ?? 0
  669. }
  670. let carbs_length = carbs?.count ?? 0
  671. var carbTotal: Decimal = 0
  672. if carbs_length != 0 {
  673. for each in carbs! {
  674. if each.carbs != 0 {
  675. carbTotal += each.carbs
  676. }
  677. }
  678. }
  679. var algo_ = "oref0" // Default
  680. if preferences.enableChris, preferences.useNewFormula {
  681. algo_ = "Dynamic ISF, Logarithmic Formula"
  682. } else if !preferences.useNewFormula, preferences.enableChris {
  683. algo_ = "Dynamic ISF, Original Formula"
  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?["NSHumanReadableCopyright"] as? String
  691. let pump_ = pumpManager?.localizedTitle ?? ""
  692. let cgm = settingsManager.settings.cgm
  693. let file = OpenAPS.Monitor.dailyStats
  694. var iPa: Decimal = 75
  695. if preferences.useCustomPeakTime {
  696. iPa = preferences.insulinPeakTime
  697. } else if preferences.curve.rawValue == "rapid-acting" {
  698. iPa = 65
  699. } else if preferences.curve.rawValue == "ultra-rapid" {
  700. iPa = 50
  701. }
  702. // Retrieve the loopStats data
  703. let lsData = storage.retrieve(OpenAPS.Monitor.loopStats, as: [LoopStats].self)?
  704. .sorted { $0.createdAt > $1.createdAt } ?? []
  705. var successRate: Double?
  706. var roundedMinutesBetweenLoops: Double?
  707. var successNR = 0.0
  708. var errorNR = 0.0
  709. var minimumInt = 999.0
  710. var maximumInt = 0.0
  711. var minimumLoopTime = 9999.0
  712. var maximumLoopTime = 0.0
  713. var timeIntervalLoops = 0.0
  714. var previousTimeLoop = Date()
  715. var endTimeForOneLoop = Date()
  716. var timeForOneLoop = 0.0
  717. var averageLoopTime = 0.0
  718. var timeForOneLoopArray: [Double] = []
  719. var medianLoopTime = 0.0
  720. var timeIntervalLoopArray: [Double] = []
  721. var medianInterval = 0.0
  722. var successIs = false
  723. if !lsData.isEmpty {
  724. var i = 0.0
  725. var j = 0.0
  726. if lsData[0].loopStatus.contains("Success") {
  727. previousTimeLoop = lsData[0].createdAt
  728. }
  729. for each in lsData {
  730. if each.loopStatus.contains("Success") {
  731. i += 1
  732. successNR += 1
  733. timeIntervalLoops = (previousTimeLoop - each.createdAt).timeInterval / 60
  734. if timeIntervalLoops > 0.0 {
  735. timeIntervalLoopArray.append(timeIntervalLoops)
  736. }
  737. endTimeForOneLoop = each.createdAt
  738. successIs = true
  739. if timeIntervalLoops > maximumInt {
  740. maximumInt = timeIntervalLoops
  741. }
  742. if timeIntervalLoops < minimumInt, timeIntervalLoops != 0.0 {
  743. minimumInt = timeIntervalLoops
  744. }
  745. previousTimeLoop = each.createdAt
  746. } else if each.loopStatus.contains("Starting") {
  747. j += 1
  748. if successIs {
  749. let test = (endTimeForOneLoop - each.createdAt).timeInterval / 60
  750. if test > 0 {
  751. timeForOneLoop = test
  752. timeForOneLoopArray.append(timeForOneLoop)
  753. averageLoopTime += timeForOneLoop
  754. timeForOneLoop = roundDouble(timeForOneLoop, 1)
  755. }
  756. if timeForOneLoop >= maximumLoopTime, timeForOneLoop != 0.0 {
  757. maximumLoopTime = timeForOneLoop
  758. }
  759. if timeForOneLoop <= minimumLoopTime, timeForOneLoop != 0.0 {
  760. minimumLoopTime = timeForOneLoop
  761. }
  762. successIs = false
  763. }
  764. } else {
  765. errorNR += 1
  766. i += 1
  767. }
  768. }
  769. successRate = (successNR / Double(i)) * 100
  770. let endI = lsData.count - 1
  771. let loopDataTime = lsData[0].createdAt - lsData[endI].createdAt
  772. let minutesBetweenLoops = (loopDataTime.timeInterval / successNR) / 60
  773. averageLoopTime /= Double(j)
  774. // Median values
  775. medianLoopTime = medianCalculation(array: timeForOneLoopArray)
  776. medianInterval = medianCalculation(array: timeIntervalLoopArray)
  777. medianInterval = roundDouble(medianInterval, 1)
  778. medianLoopTime = roundDouble(medianLoopTime, 1)
  779. averageLoopTime = roundDouble(averageLoopTime, 1)
  780. roundedMinutesBetweenLoops = roundDouble(minutesBetweenLoops, 1)
  781. minimumInt = roundDouble(minimumInt, 1)
  782. maximumInt = roundDouble(maximumInt, 1)
  783. }
  784. // Time In Range (%) and Average Glucose (24 hours). This looks dumb and I will refactor it later.
  785. let glucose = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self)
  786. let length_ = glucose?.count ?? 0
  787. let endIndex = length_ - 1
  788. var oneDayGlucoseIndex = endIndex
  789. var bg: Decimal = 0
  790. var bgArray: [Double] = []
  791. var medianBG = 0.0
  792. var nr_bgs: Decimal = 0
  793. let startDate = glucose![0].date
  794. var end1 = false
  795. var end7 = false
  796. var end10 = false
  797. var bg_1: Decimal = 0
  798. var bg_7: Decimal = 0
  799. var bg_10: Decimal = 0
  800. var bg_total: Decimal = 0
  801. var j = -1
  802. if length_ != 0 {
  803. for entry in glucose! {
  804. j += 1
  805. if entry.glucose! > 0 {
  806. bg += Decimal(entry.glucose!)
  807. bgArray.append(Double(entry.glucose!))
  808. nr_bgs += 1
  809. if startDate - entry.date >= 8.64E7, !end1 {
  810. end1 = true
  811. oneDayGlucoseIndex = j
  812. bg_1 = bg / nr_bgs
  813. }
  814. if startDate - entry.date >= 6.045E8, !end7 {
  815. end7 = true
  816. bg_7 = bg / nr_bgs
  817. }
  818. if startDate - entry.date >= 8.64E8, !end10 {
  819. end10 = true
  820. bg_10 = bg / nr_bgs
  821. }
  822. }
  823. }
  824. }
  825. if nr_bgs != 0 {
  826. bg_total = bg / nr_bgs
  827. }
  828. medianBG = medianCalculation(array: bgArray)
  829. let fullTime = glucose![0].date - glucose![endIndex].date
  830. let fullTime_1 = glucose![0].date - glucose![oneDayGlucoseIndex].date
  831. var daysBG = fullTime / 8.64E7
  832. var timeInHypo: Decimal = 0
  833. var timeInHyper: Decimal = 0
  834. var hypos: Decimal = 0
  835. var hypers: Decimal = 0
  836. var i = -1
  837. var lastIndex = false
  838. while i < endIndex {
  839. i += 1
  840. let currentTime = glucose![i].date
  841. var previousTime = currentTime
  842. if i + 1 <= endIndex {
  843. previousTime = glucose![i + 1].date
  844. } else {
  845. lastIndex = true
  846. }
  847. if glucose![i].glucose! < 72, !lastIndex {
  848. timeInHypo += currentTime - previousTime
  849. } else if glucose![i].glucose! > 180, !lastIndex {
  850. timeInHyper += currentTime - previousTime
  851. }
  852. }
  853. if timeInHypo == 0 {
  854. hypos = 0
  855. } else { hypos = (timeInHypo / fullTime) * 100
  856. }
  857. if timeInHyper == 0 {
  858. hypers = 0
  859. } else { hypers = (timeInHyper / fullTime) * 100
  860. }
  861. let TIR = 100 - (hypos + hypers)
  862. // Do the loop again but with for 1 day. I will change this later, because this looks really dumb:
  863. var timeInHypo_1: Decimal = 0
  864. var timeInHyper_1: Decimal = 0
  865. var hypos_1: Decimal = 0
  866. var hypers_1: Decimal = 0
  867. i = -1
  868. lastIndex = false
  869. while i < oneDayGlucoseIndex {
  870. i += 1
  871. let currentTime = glucose![i].date
  872. var previousTime = currentTime
  873. if i + 1 <= oneDayGlucoseIndex {
  874. previousTime = glucose![i + 1].date
  875. } else {
  876. lastIndex = true
  877. }
  878. if glucose![i].glucose! < 72, !lastIndex {
  879. timeInHypo_1 += currentTime - previousTime
  880. } else if glucose![i].glucose! > 180, !lastIndex {
  881. timeInHyper_1 += currentTime - previousTime
  882. }
  883. }
  884. if timeInHypo_1 == 0 {
  885. hypos_1 = 0
  886. } else { hypos_1 = (timeInHypo_1 / fullTime_1) * 100
  887. }
  888. if timeInHyper_1 == 0 {
  889. hypers_1 = 0
  890. } else { hypers_1 = (timeInHyper_1 / fullTime_1) * 100
  891. }
  892. let TIR_1 = 100 - (hypos_1 + hypers_1)
  893. // Add 10 day average to tenDaysStats.json
  894. let file_10 = OpenAPS.Monitor.tenDaysStats
  895. // let tensDaysStats = storage.retrieve(file_10, as: [TenDaysStats].self)
  896. let tenStats = TenDaysStats(
  897. createdAt: Date(), past10daysAverage: roundDecimal(bg_10, 1)
  898. )
  899. var uniqEvents: [TenDaysStats] = []
  900. var test1 = uniqEvents
  901. let test2: [TenDaysStats] = [tenStats]
  902. var countIndeces = 0
  903. storage.transaction { storage in
  904. test1 = storage.retrieve(file_10, as: [TenDaysStats].self) ?? []
  905. countIndeces = test1.count
  906. }
  907. if daysBG >= 10 {
  908. if countIndeces == 0 {
  909. storage.transaction { storage in
  910. storage.save(test2, as: file_10)
  911. }
  912. // Keep 10 days apart from each array
  913. } else if test1[0].createdAt.addingTimeInterval(10.days.timeInterval) < Date() {
  914. storage.transaction { storage in
  915. storage.append(tenStats, to: file_10, uniqBy: \.createdAt)
  916. uniqEvents = storage.retrieve(file_10, as: [TenDaysStats].self)?
  917. .filter { $0.createdAt.addingTimeInterval(365.days.timeInterval) > Date() }
  918. .sorted { $0.createdAt > $1.createdAt } ?? []
  919. storage.save(Array(uniqEvents), as: file_10)
  920. }
  921. }
  922. }
  923. // Retrieve the 10 days data array
  924. let uniqEvents_1 = storage.retrieve(OpenAPS.Monitor.tenDaysStats, as: [TenDaysStats].self)?
  925. .filter { $0.createdAt.addingTimeInterval(365.days.timeInterval) > Date() }
  926. .sorted { $0.createdAt > $1.createdAt } ?? []
  927. var index = 0
  928. var total: Decimal = 0
  929. var thirtyDays: Decimal = 0
  930. var ninetyDays: Decimal = 0
  931. for uniqEvent in uniqEvents_1 {
  932. if uniqEvent.past10daysAverage != 0 {
  933. total += uniqEvent.past10daysAverage
  934. index += 1
  935. }
  936. if index == 3 {
  937. thirtyDays = total / 3
  938. }
  939. if index == 9 {
  940. ninetyDays = total / 9
  941. }
  942. }
  943. // HbA1c estimation (%, mmol/mol)
  944. let NGSPa1CStatisticValue = (46.7 + bg_1) / 28.7 // NGSP (%)
  945. let IFCCa1CStatisticValue = 10.929 *
  946. (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol) A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
  947. // 7 days
  948. let NGSPa1CStatisticValue_7 = (46.7 + bg_7) / 28.7
  949. let IFCCa1CStatisticValue_7 = 10.929 * (NGSPa1CStatisticValue_7 - 2.152)
  950. // 30 days
  951. let NGSPa1CStatisticValue_30 = (46.7 + thirtyDays) / 28.7
  952. let IFCCa1CStatisticValue_30 = 10.929 * (NGSPa1CStatisticValue_30 - 2.152)
  953. // Total days (up t0 10 days)
  954. let NGSPa1CStatisticValue_total = (46.7 + bg_total) / 28.7
  955. let IFCCa1CStatisticValue_total = 10.929 * (NGSPa1CStatisticValue_total - 2.152)
  956. // 90 Days
  957. let NGSPa1CStatisticValue_90 = (46.7 + ninetyDays) / 28.7
  958. let IFCCa1CStatisticValue_90 = 10.929 * (NGSPa1CStatisticValue_90 - 2.152)
  959. // HbA1c string and BG string:
  960. var HbA1c_string_1 = ""
  961. var string7Days = ""
  962. var string30Days = ""
  963. var string90Days = ""
  964. var stringTotal = ""
  965. var bgString1day = ""
  966. var bgString7Days = ""
  967. var bgString30Days = ""
  968. var bgString90Days = ""
  969. var bgAverageTotalString = ""
  970. var loopString = ""
  971. // round output values
  972. daysBG = roundDecimal(daysBG, 1)
  973. if bg_1 != 0 {
  974. bgString1day =
  975. " Average BG (mmol/l) 24 hours): \(roundDecimal(bg_1 * 0.0555, 1)). Average BG (mmg/dl) 24 hours: \(roundDecimal(bg_1, 0))."
  976. HbA1c_string_1 =
  977. "Estimated HbA1c (mmol/mol, 1 day): \(roundDecimal(IFCCa1CStatisticValue, 1)). Estimated HbA1c (%, 1 day): \(roundDecimal(NGSPa1CStatisticValue, 1)). "
  978. }
  979. if bg_7 != 0 {
  980. string7Days =
  981. " HbA1c 7 days (mmol/mol): \(roundDecimal(IFCCa1CStatisticValue_7, 1)). HbA1c 7 days (%): \(roundDecimal(NGSPa1CStatisticValue_7, 1))."
  982. bgString7Days =
  983. " Average BG (mmol/l) 7 days: \(roundDecimal(bg_7 * 0.0555, 1)). Average BG (mg/dl) 7 days: \(roundDecimal(bg_7, 0))."
  984. }
  985. if thirtyDays != 0 {
  986. string30Days =
  987. " HbA1c 30 days (mmol/mol): \(roundDecimal(IFCCa1CStatisticValue_30, 1)). HbA1c 30 days (%): \(roundDecimal(NGSPa1CStatisticValue_30, 1))."
  988. bgString30Days =
  989. " Average BG 30 days (mmol/l): \(roundDecimal(thirtyDays * 0.0555, 1)). Average BG 30 days (mg/dl): \(roundDecimal(thirtyDays, 0)). "
  990. }
  991. if ninetyDays != 0 {
  992. string90Days =
  993. " HbA1c 90 days (mmol/mol): \(roundDecimal(IFCCa1CStatisticValue_90, 1)). HbA1c 90 days (%): \(roundDecimal(NGSPa1CStatisticValue_90, 1))."
  994. bgString90Days =
  995. " Average BG 90 days (mmol/l): \(roundDecimal(ninetyDays * 0.0555, 1)). Average BG 90 days (mg/dl): \(roundDecimal(ninetyDays, 0)). "
  996. }
  997. if bg_total != 0, daysBG >= 2 {
  998. stringTotal =
  999. " HbA1c Total (\(daysBG)) Days (mmol/mol): \(roundDecimal(IFCCa1CStatisticValue_total, 1)). HbA1c Total (\(daysBG)) Days (mg/dl): \(roundDecimal(NGSPa1CStatisticValue_total, 1)) %."
  1000. bgAverageTotalString =
  1001. " BG Median Total (\(daysBG)) Days (mmol/l): \(roundDouble(medianBG * 0.0555, 1)). BG Median Total (\(daysBG)) Days (mg/dl): \(roundDouble(medianBG, 0)). BG Average Total (\(daysBG)) Days (mmg/dl): \(roundDecimal(bg_total, 0))."
  1002. }
  1003. let HbA1c_string = HbA1c_string_1 + string7Days + string30Days + string90Days + stringTotal
  1004. var tirString =
  1005. "TIR (24 hours): \(roundDecimal(TIR_1, 0)) %. Time with Hypoglycaemia: \(roundDecimal(hypos_1, 0)) % (< 4 / 72). Time with Hyperglycaemia: \(roundDecimal(hypers_1, 0)) % (> 10 / 180)."
  1006. if daysBG >= 2 {
  1007. tirString +=
  1008. " Total days (\(daysBG) TIR: \(roundDecimal(TIR, 1)) %. Time with Hypoglycaemia: \(roundDecimal(hypos, 1)) % (< 4 / 72). Time with Hyperglycaemia: \(roundDecimal(hypers, 1)) % (> 10 / 180)."
  1009. }
  1010. let bgAverageString = bgString1day + bgString7Days + bgString30Days + bgString90Days + bgAverageTotalString
  1011. var minString = ""
  1012. if minimumInt == 999.00 {
  1013. minString = "Shortest Time Interval: N/A min, "
  1014. } else { minString = "Shortest Time Interval: \(minimumInt) min," }
  1015. var maxString = ""
  1016. if maximumInt == 0.0 {
  1017. maxString = "Longest Time Interval: N/A min."
  1018. } else { maxString = "Longest Time Interval: \(maximumInt) min." }
  1019. if minimumLoopTime != 9999.0 {
  1020. loopString += " Shortest Loop: \(minimumLoopTime) min. "
  1021. }
  1022. if maximumLoopTime != 0.0 {
  1023. loopString += "Longest Loop: \(maximumLoopTime) min."
  1024. }
  1025. let dailystat = DailyStats(
  1026. createdAt: Date(),
  1027. iPhone: UIDevice.current.getDeviceId,
  1028. iOS: UIDevice.current.getOSInfo,
  1029. Build_Version: version ?? "",
  1030. Build_Number: build ?? "1",
  1031. Branch: branch ?? "N/A",
  1032. Build_Date: buildDate,
  1033. Algorithm: algo_,
  1034. AdjustmentFactor: af,
  1035. Pump: pump_,
  1036. CGM: cgm.rawValue,
  1037. insulinType: insulin_type.rawValue,
  1038. peakActivityTime: iPa,
  1039. TDD: roundDecimal(currentTDD, 2),
  1040. Carbs_24h: carbTotal,
  1041. TIR: tirString,
  1042. BG_Average: bgAverageString,
  1043. HbA1c: HbA1c_string,
  1044. Loop_Cycles: "Success Rate : \(round(successRate ?? 0)) %. Loops/Errors: \(Int(successNR))/\(Int(errorNR)). Median Time Between Loop Cycles: \(medianInterval) min. Average Time Between Loop Cycles: \(roundedMinutesBetweenLoops ?? 0) min. " +
  1045. minString + maxString + loopString +
  1046. " Median Loop Duration: \(medianLoopTime) min. Average Loop Duration: \(averageLoopTime) min. "
  1047. )
  1048. var uniqeEvents: [DailyStats] = []
  1049. storage.transaction { storage in
  1050. storage.append(dailystat, to: file, uniqBy: \.createdAt)
  1051. uniqeEvents = storage.retrieve(file, as: [DailyStats].self)?
  1052. .filter { $0.createdAt.addingTimeInterval(24.hours.timeInterval) > Date() }
  1053. .sorted { $0.createdAt > $1.createdAt } ?? []
  1054. storage.save(Array(uniqeEvents), as: file)
  1055. }
  1056. }
  1057. private func loopStats(error: Error? = nil, starting: Bool) {
  1058. let file = OpenAPS.Monitor.loopStats
  1059. var errString = "Success"
  1060. if let error = error {
  1061. errString = error.localizedDescription
  1062. }
  1063. if starting {
  1064. errString = "Starting"
  1065. }
  1066. let loopstat = LoopStats(
  1067. createdAt: Date(),
  1068. loopStatus: errString
  1069. )
  1070. var uniqEvents: [LoopStats] = []
  1071. storage.transaction { storage in
  1072. storage.append(loopstat, to: file, uniqBy: \.createdAt)
  1073. uniqEvents = storage.retrieve(file, as: [LoopStats].self)?
  1074. .filter { $0.createdAt.addingTimeInterval(24.hours.timeInterval) > Date() }
  1075. .sorted { $0.createdAt > $1.createdAt } ?? []
  1076. storage.save(Array(uniqEvents), as: file)
  1077. }
  1078. }
  1079. private func processError(_ error: Error) {
  1080. warning(.apsManager, "\(error.localizedDescription)")
  1081. lastError.send(error)
  1082. }
  1083. private func createBolusReporter() {
  1084. bolusReporter = pumpManager?.createBolusProgressReporter(reportingOn: processQueue)
  1085. bolusReporter?.addObserver(self)
  1086. }
  1087. private func updateStatus() {
  1088. debug(.apsManager, "force update status")
  1089. guard let pump = pumpManager else {
  1090. return
  1091. }
  1092. if let omnipod = pump as? OmnipodPumpManager {
  1093. omnipod.getPodStatus { _ in }
  1094. }
  1095. if let omnipodBLE = pump as? OmniBLEPumpManager {
  1096. omnipodBLE.getPodStatus { _ in }
  1097. }
  1098. }
  1099. private func clearBolusReporter() {
  1100. bolusReporter?.removeObserver(self)
  1101. bolusReporter = nil
  1102. processQueue.asyncAfter(deadline: .now() + 0.5) {
  1103. self.bolusProgress.send(nil)
  1104. self.updateStatus()
  1105. }
  1106. }
  1107. }
  1108. private extension PumpManager {
  1109. func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval) -> AnyPublisher<DoseEntry?, Error> {
  1110. Future { promise in
  1111. self.enactTempBasal(unitsPerHour: unitsPerHour, for: duration) { error in
  1112. if let error = error {
  1113. debug(.apsManager, "Temp basal failed: \(unitsPerHour) for: \(duration)")
  1114. promise(.failure(error))
  1115. } else {
  1116. debug(.apsManager, "Temp basal succeded: \(unitsPerHour) for: \(duration)")
  1117. promise(.success(nil))
  1118. }
  1119. }
  1120. }
  1121. .mapError { APSError.pumpError($0) }
  1122. .eraseToAnyPublisher()
  1123. }
  1124. func enactBolus(units: Double, automatic: Bool) -> AnyPublisher<DoseEntry?, Error> {
  1125. Future { promise in
  1126. // convert automatic
  1127. let automaticValue = automatic ? BolusActivationType.automatic : BolusActivationType.manualRecommendationAccepted
  1128. self.enactBolus(units: units, activationType: automaticValue) { error in
  1129. if let error = error {
  1130. debug(.apsManager, "Bolus failed: \(units)")
  1131. promise(.failure(error))
  1132. } else {
  1133. debug(.apsManager, "Bolus succeded: \(units)")
  1134. promise(.success(nil))
  1135. }
  1136. }
  1137. }
  1138. .mapError { APSError.pumpError($0) }
  1139. .eraseToAnyPublisher()
  1140. }
  1141. func cancelBolus() -> AnyPublisher<DoseEntry?, Error> {
  1142. Future { promise in
  1143. self.cancelBolus { result in
  1144. switch result {
  1145. case let .success(dose):
  1146. debug(.apsManager, "Cancel Bolus succeded")
  1147. promise(.success(dose))
  1148. case let .failure(error):
  1149. debug(.apsManager, "Cancel Bolus failed")
  1150. promise(.failure(error))
  1151. }
  1152. }
  1153. }
  1154. .mapError { APSError.pumpError($0) }
  1155. .eraseToAnyPublisher()
  1156. }
  1157. func suspendDelivery() -> AnyPublisher<Void, Error> {
  1158. Future { promise in
  1159. self.suspendDelivery { error in
  1160. if let error = error {
  1161. promise(.failure(error))
  1162. } else {
  1163. promise(.success(()))
  1164. }
  1165. }
  1166. }
  1167. .mapError { APSError.pumpError($0) }
  1168. .eraseToAnyPublisher()
  1169. }
  1170. func resumeDelivery() -> AnyPublisher<Void, Error> {
  1171. Future { promise in
  1172. self.resumeDelivery { error in
  1173. if let error = error {
  1174. promise(.failure(error))
  1175. } else {
  1176. promise(.success(()))
  1177. }
  1178. }
  1179. }
  1180. .mapError { APSError.pumpError($0) }
  1181. .eraseToAnyPublisher()
  1182. }
  1183. }
  1184. extension BaseAPSManager: PumpManagerStatusObserver {
  1185. func pumpManager(_: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) {
  1186. let percent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100)
  1187. let battery = Battery(
  1188. percent: percent,
  1189. voltage: nil,
  1190. string: percent > 10 ? .normal : .low,
  1191. display: status.pumpBatteryChargeRemaining != nil
  1192. )
  1193. storage.save(battery, as: OpenAPS.Monitor.battery)
  1194. storage.save(status.pumpStatus, as: OpenAPS.Monitor.status)
  1195. }
  1196. }
  1197. extension BaseAPSManager: DoseProgressObserver {
  1198. func doseProgressReporterDidUpdate(_ doseProgressReporter: DoseProgressReporter) {
  1199. bolusProgress.send(Decimal(doseProgressReporter.progress.percentComplete))
  1200. if doseProgressReporter.progress.isComplete {
  1201. clearBolusReporter()
  1202. }
  1203. }
  1204. }
  1205. extension PumpManagerStatus {
  1206. var pumpStatus: PumpStatus {
  1207. let bolusing = bolusState != .noBolus
  1208. let suspended = basalDeliveryState?.isSuspended ?? true
  1209. let type = suspended ? StatusType.suspended : (bolusing ? .bolusing : .normal)
  1210. return PumpStatus(status: type, bolusing: bolusing, suspended: suspended, timestamp: Date())
  1211. }
  1212. }