APSManager.swift 56 KB

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