APSManager.swift 52 KB

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