NightscoutManager.swift 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import LoopKitUI
  5. import Swinject
  6. import UIKit
  7. protocol NightscoutManager: GlucoseSource {
  8. func fetchGlucose(since date: Date) -> AnyPublisher<[BloodGlucose], Never>
  9. func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never>
  10. func fetchTempTargets() -> AnyPublisher<[TempTarget], Never>
  11. func fetchAnnouncements() -> AnyPublisher<[Announcement], Never>
  12. func deleteCarbs(_ treatment: DataTable.Treatment, complexMeal: Bool)
  13. func deleteInsulin(at date: Date)
  14. func deleteManualGlucose(at: Date)
  15. func uploadStatus()
  16. func uploadGlucose()
  17. func uploadManualGlucose()
  18. func uploadStatistics(dailystat: Statistics)
  19. func uploadPreferences(_ preferences: Preferences)
  20. func uploadProfileAndSettings(_: Bool)
  21. var cgmURL: URL? { get }
  22. }
  23. final class BaseNightscoutManager: NightscoutManager, Injectable {
  24. @Injected() private var keychain: Keychain!
  25. @Injected() private var glucoseStorage: GlucoseStorage!
  26. @Injected() private var tempTargetsStorage: TempTargetsStorage!
  27. @Injected() private var carbsStorage: CarbsStorage!
  28. @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
  29. @Injected() private var storage: FileStorage!
  30. @Injected() private var announcementsStorage: AnnouncementsStorage!
  31. @Injected() private var settingsManager: SettingsManager!
  32. @Injected() private var broadcaster: Broadcaster!
  33. @Injected() private var reachabilityManager: ReachabilityManager!
  34. @Injected() var healthkitManager: HealthKitManager!
  35. private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
  36. private var ping: TimeInterval?
  37. private var lifetime = Lifetime()
  38. private var isNetworkReachable: Bool {
  39. reachabilityManager.isReachable
  40. }
  41. private var isUploadEnabled: Bool {
  42. settingsManager.settings.isUploadEnabled
  43. }
  44. private var isUploadGlucoseEnabled: Bool {
  45. settingsManager.settings.uploadGlucose
  46. }
  47. private var nightscoutAPI: NightscoutAPI? {
  48. guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
  49. let url = URL(string: urlString),
  50. let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
  51. else {
  52. return nil
  53. }
  54. return NightscoutAPI(url: url, secret: secret)
  55. }
  56. private let context = CoreDataStack.shared.persistentContainer.newBackgroundContext()
  57. private var lastTwoDeterminations: [OrefDetermination]?
  58. init(resolver: Resolver) {
  59. injectServices(resolver)
  60. subscribe()
  61. }
  62. private func subscribe() {
  63. broadcaster.register(PumpHistoryObserver.self, observer: self)
  64. broadcaster.register(CarbsObserver.self, observer: self)
  65. broadcaster.register(TempTargetsObserver.self, observer: self)
  66. broadcaster.register(GlucoseObserver.self, observer: self)
  67. _ = reachabilityManager.startListening(onQueue: processQueue) { status in
  68. debug(.nightscout, "Network status: \(status)")
  69. }
  70. }
  71. func sourceInfo() -> [String: Any]? {
  72. if let ping = ping {
  73. return [GlucoseSourceKey.nightscoutPing.rawValue: ping]
  74. }
  75. return nil
  76. }
  77. var cgmURL: URL? {
  78. if let url = settingsManager.settings.cgm.appURL {
  79. return url
  80. }
  81. let useLocal = settingsManager.settings.useLocalGlucoseSource
  82. let maybeNightscout = useLocal
  83. ? NightscoutAPI(url: URL(string: "http://127.0.0.1:\(settingsManager.settings.localGlucosePort)")!)
  84. : nightscoutAPI
  85. return maybeNightscout?.url
  86. }
  87. func fetchGlucose(since date: Date) -> AnyPublisher<[BloodGlucose], Never> {
  88. let useLocal = settingsManager.settings.useLocalGlucoseSource
  89. ping = nil
  90. if !useLocal {
  91. guard isNetworkReachable else {
  92. return Just([]).eraseToAnyPublisher()
  93. }
  94. }
  95. let maybeNightscout = useLocal
  96. ? NightscoutAPI(url: URL(string: "http://127.0.0.1:\(settingsManager.settings.localGlucosePort)")!)
  97. : nightscoutAPI
  98. guard let nightscout = maybeNightscout else {
  99. return Just([]).eraseToAnyPublisher()
  100. }
  101. let startDate = Date()
  102. return nightscout.fetchLastGlucose(sinceDate: date)
  103. .tryCatch({ (error) -> AnyPublisher<[BloodGlucose], Error> in
  104. print(error.localizedDescription)
  105. return Just([]).setFailureType(to: Error.self).eraseToAnyPublisher()
  106. })
  107. .replaceError(with: [])
  108. .handleEvents(receiveOutput: { value in
  109. guard value.isNotEmpty else { return }
  110. self.ping = Date().timeIntervalSince(startDate)
  111. })
  112. .eraseToAnyPublisher()
  113. }
  114. // MARK: - GlucoseSource
  115. var glucoseManager: FetchGlucoseManager?
  116. var cgmManager: CGMManagerUI?
  117. var cgmType: CGMType = .nightscout
  118. func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
  119. fetchGlucose(since: glucoseStorage.syncDate())
  120. }
  121. func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
  122. fetch(nil)
  123. }
  124. func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never> {
  125. guard let nightscout = nightscoutAPI, isNetworkReachable else {
  126. return Just([]).eraseToAnyPublisher()
  127. }
  128. let since = carbsStorage.syncDate()
  129. return nightscout.fetchCarbs(sinceDate: since)
  130. .replaceError(with: [])
  131. .eraseToAnyPublisher()
  132. }
  133. func fetchTempTargets() -> AnyPublisher<[TempTarget], Never> {
  134. guard let nightscout = nightscoutAPI, isNetworkReachable else {
  135. return Just([]).eraseToAnyPublisher()
  136. }
  137. let since = tempTargetsStorage.syncDate()
  138. return nightscout.fetchTempTargets(sinceDate: since)
  139. .replaceError(with: [])
  140. .eraseToAnyPublisher()
  141. }
  142. func fetchAnnouncements() -> AnyPublisher<[Announcement], Never> {
  143. guard let nightscout = nightscoutAPI, isNetworkReachable else {
  144. return Just([]).eraseToAnyPublisher()
  145. }
  146. let since = announcementsStorage.syncDate()
  147. return nightscout.fetchAnnouncement(sinceDate: since)
  148. .replaceError(with: [])
  149. .eraseToAnyPublisher()
  150. }
  151. func deleteCarbs(_ treatment: DataTable.Treatment, complexMeal: Bool) {
  152. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  153. carbsStorage.deleteCarbs(at: treatment.id, fpuID: treatment.fpuID ?? "", complex: complexMeal)
  154. return
  155. }
  156. print("meals 3: ID: " + (treatment.id ?? "").description + " FPU ID: " + (treatment.fpuID ?? "").description)
  157. var arg1 = ""
  158. var arg2 = ""
  159. if complexMeal {
  160. arg1 = treatment.id ?? ""
  161. arg2 = treatment.fpuID ?? ""
  162. } else if treatment.isFPU ?? false {
  163. arg1 = ""
  164. arg2 = treatment.fpuID ?? ""
  165. } else {
  166. arg1 = treatment.id
  167. arg2 = ""
  168. }
  169. healthkitManager.deleteCarbs(syncID: arg1, fpuID: arg2)
  170. if complexMeal {
  171. nightscout.deleteCarbs(treatment)
  172. .collect()
  173. .sink { completion in
  174. self.carbsStorage.deleteCarbs(at: treatment.id ?? "", fpuID: treatment.fpuID ?? "", complex: true)
  175. switch completion {
  176. case .finished:
  177. debug(.nightscout, "Carbs deleted")
  178. case let .failure(error):
  179. info(
  180. .nightscout,
  181. "Deletion of carbs in NightScout not done \n \(error.localizedDescription)",
  182. type: MessageType.warning
  183. )
  184. }
  185. } receiveValue: { _ in }
  186. .store(in: &lifetime)
  187. if (treatment.fpuID ?? "") != "" {
  188. nightscout.deleteCarbs(treatment)
  189. .collect()
  190. .sink { completion in
  191. switch completion {
  192. case .finished:
  193. debug(.nightscout, "Carb equivalents deleted from NS")
  194. case let .failure(error):
  195. info(
  196. .nightscout,
  197. "Deletion of carb equivalents in NightScout not done \n \(error.localizedDescription)",
  198. type: MessageType.warning
  199. )
  200. }
  201. } receiveValue: { _ in }
  202. .store(in: &lifetime)
  203. }
  204. } else if treatment.isFPU ?? false {
  205. nightscout.deleteCarbs(treatment)
  206. .collect()
  207. .sink { completion in
  208. self.carbsStorage.deleteCarbs(at: "", fpuID: treatment.fpuID ?? "", complex: false)
  209. switch completion {
  210. case .finished:
  211. debug(.nightscout, "Carb equivalents deleted")
  212. case let .failure(error):
  213. info(
  214. .nightscout,
  215. "Deletion of carb equivalents in NightScout not done \n \(error.localizedDescription)",
  216. type: MessageType.warning
  217. )
  218. }
  219. } receiveValue: { _ in }
  220. .store(in: &lifetime)
  221. } else {
  222. nightscout.deleteCarbs(treatment)
  223. .collect()
  224. .sink { completion in
  225. self.carbsStorage.deleteCarbs(at: treatment.id, fpuID: "", complex: false)
  226. switch completion {
  227. case .finished:
  228. debug(.nightscout, "Carbs deleted")
  229. case let .failure(error):
  230. info(
  231. .nightscout,
  232. "Deletion of carbs in NightScout not done \n \(error.localizedDescription)",
  233. type: MessageType.warning
  234. )
  235. }
  236. } receiveValue: { _ in }
  237. .store(in: &lifetime)
  238. }
  239. }
  240. func deleteInsulin(at date: Date) {
  241. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  242. pumpHistoryStorage.deleteInsulin(at: date)
  243. return
  244. }
  245. nightscout.deleteInsulin(at: date)
  246. .sink { completion in
  247. switch completion {
  248. case .finished:
  249. self.pumpHistoryStorage.deleteInsulin(at: date)
  250. debug(.nightscout, "Carbs deleted")
  251. case let .failure(error):
  252. debug(.nightscout, error.localizedDescription)
  253. }
  254. } receiveValue: {}
  255. .store(in: &lifetime)
  256. }
  257. func deleteManualGlucose(at date: Date) {
  258. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  259. return
  260. }
  261. nightscout.deleteManualGlucose(at: date)
  262. .sink { completion in
  263. switch completion {
  264. case .finished:
  265. debug(.nightscout, "Manual Glucose entry deleted")
  266. case let .failure(error):
  267. debug(.nightscout, error.localizedDescription)
  268. }
  269. } receiveValue: {}
  270. .store(in: &lifetime)
  271. }
  272. func uploadStatistics(dailystat: Statistics) {
  273. let stats = NightscoutStatistics(
  274. dailystats: dailystat
  275. )
  276. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  277. return
  278. }
  279. processQueue.async {
  280. nightscout.uploadStats(stats)
  281. .sink { completion in
  282. switch completion {
  283. case .finished:
  284. debug(.nightscout, "Statistics uploaded")
  285. case let .failure(error):
  286. debug(.nightscout, error.localizedDescription)
  287. }
  288. } receiveValue: {}
  289. .store(in: &self.lifetime)
  290. }
  291. }
  292. func uploadPreferences(_ preferences: Preferences) {
  293. let prefs = NightscoutPreferences(
  294. preferences: settingsManager.preferences
  295. )
  296. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  297. return
  298. }
  299. processQueue.async {
  300. nightscout.uploadPrefs(prefs)
  301. .sink { completion in
  302. switch completion {
  303. case .finished:
  304. debug(.nightscout, "Preferences uploaded")
  305. self.storage.save(preferences, as: OpenAPS.Nightscout.uploadedPreferences)
  306. case let .failure(error):
  307. debug(.nightscout, error.localizedDescription)
  308. }
  309. } receiveValue: {}
  310. .store(in: &self.lifetime)
  311. }
  312. }
  313. func uploadSettings(_ settings: FreeAPSSettings) {
  314. let sets = NightscoutSettings(
  315. settings: settingsManager.settings
  316. )
  317. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  318. return
  319. }
  320. processQueue.async {
  321. nightscout.uploadSettings(sets)
  322. .sink { completion in
  323. switch completion {
  324. case .finished:
  325. debug(.nightscout, "Settings uploaded")
  326. self.storage.save(settings, as: OpenAPS.Nightscout.uploadedSettings)
  327. case let .failure(error):
  328. debug(.nightscout, error.localizedDescription)
  329. }
  330. } receiveValue: {}
  331. .store(in: &self.lifetime)
  332. }
  333. }
  334. private func fetchBattery() -> Battery {
  335. do {
  336. let results = try context.fetch(OpenAPS_Battery.fetch(NSPredicate.predicateFor30MinAgo))
  337. if let last = results.first {
  338. let percent: Int? = Int(last.percent)
  339. let voltage: Decimal? = last.voltage as Decimal?
  340. let status: String? = last.status
  341. let display: Bool? = last.display
  342. if let percent = percent, let voltage = voltage, let status = status, let display = display {
  343. debugPrint(
  344. "Home State Model: \(#function) \(DebuggingIdentifiers.succeeded) setup battery from core data successfully"
  345. )
  346. return Battery(
  347. percent: percent,
  348. voltage: voltage,
  349. string: BatteryState(rawValue: status) ?? BatteryState.normal,
  350. display: display
  351. )
  352. }
  353. }
  354. return Battery(percent: 100, voltage: 100, string: BatteryState.normal, display: false)
  355. } catch {
  356. debugPrint(
  357. "Home State Model: \(#function) \(DebuggingIdentifiers.failed) failed to setup battery from core data"
  358. )
  359. return Battery(percent: 100, voltage: 100, string: BatteryState.normal, display: false)
  360. }
  361. }
  362. private func fetchDeterminations() {
  363. let fetchRequest: NSFetchRequest<OrefDetermination> = OrefDetermination.fetchRequest()
  364. fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \OrefDetermination.deliverAt, ascending: false)]
  365. fetchRequest.predicate = NSPredicate.predicateFor30MinAgoForDetermination
  366. fetchRequest.fetchLimit = 2
  367. do {
  368. lastTwoDeterminations = try context.fetch(fetchRequest)
  369. debugPrint(
  370. "Home State Model: \(#function) \(DebuggingIdentifiers.succeeded) fetched determinations from core data"
  371. )
  372. } catch {
  373. debugPrint(
  374. "Home State Model: \(#function) \(DebuggingIdentifiers.failed) failed to fetch determinations from core data"
  375. )
  376. }
  377. }
  378. func uploadStatus() {
  379. let iob = storage.retrieve(OpenAPS.Monitor.iob, as: [IOBEntry].self)
  380. let penultimateDetermination = lastTwoDeterminations?.last
  381. let lastDetermination = lastTwoDeterminations?.first
  382. var suggested: Determination?
  383. var enacted: Determination?
  384. if let lastDetermination = lastDetermination, let penultimateDetermination = penultimateDetermination {
  385. if lastDetermination.enacted, penultimateDetermination.enacted {
  386. suggested = Determination(
  387. reason: lastDetermination.reason ?? "",
  388. units: lastDetermination.smbToDeliver?.decimalValue,
  389. insulinReq: lastDetermination.insulinReq?.decimalValue,
  390. eventualBG: Int(truncating: lastDetermination.eventualBG ?? 0),
  391. sensitivityRatio: lastDetermination.sensitivityRatio?.decimalValue,
  392. rate: lastDetermination.rate?.decimalValue,
  393. duration: Int(lastDetermination.duration),
  394. iob: lastDetermination.iob?.decimalValue,
  395. cob: Decimal(lastDetermination.cob),
  396. predictions: nil,
  397. deliverAt: lastDetermination.deliverAt ?? Date(),
  398. carbsReq: Decimal(lastDetermination.carbsRequired),
  399. temp: TempType(rawValue: lastDetermination.temp ?? ""),
  400. bg: lastDetermination.glucose?.decimalValue,
  401. reservoir: lastDetermination.reservoir?.decimalValue,
  402. isf: lastDetermination.insulinSensitivity?.decimalValue,
  403. timestamp: lastDetermination.timestamp,
  404. recieved: lastDetermination.received,
  405. tdd: lastDetermination.totalDailyDose?.decimalValue ?? Decimal(0),
  406. insulin: Insulin(
  407. TDD: lastDetermination.totalDailyDose?.decimalValue ?? Decimal(0),
  408. bolus: lastDetermination.bolus?.decimalValue ?? Decimal(0),
  409. temp_basal: lastDetermination.tempBasal?.decimalValue ?? Decimal(0),
  410. scheduled_basal: lastDetermination.scheduledBasal?.decimalValue ?? Decimal(0)
  411. ),
  412. current_target: lastDetermination.currentTarget?.decimalValue ?? Decimal(0),
  413. insulinForManualBolus: lastDetermination.insulinForManualBolus?.decimalValue ?? Decimal(0),
  414. manualBolusErrorString: lastDetermination.manualBolusErrorString?.decimalValue ?? Decimal(0),
  415. minDelta: lastDetermination.minDelta?.decimalValue ?? Decimal(0),
  416. expectedDelta: lastDetermination.expectedDelta?.decimalValue ?? Decimal(0),
  417. minGuardBG: nil, minPredBG: nil, threshold: lastDetermination.threshold?.decimalValue ?? Decimal(0),
  418. carbRatio: lastDetermination.carbRatio?.decimalValue ?? Decimal(0)
  419. )
  420. enacted = Determination(
  421. reason: lastDetermination.reason ?? "",
  422. units: lastDetermination.smbToDeliver?.decimalValue,
  423. insulinReq: lastDetermination.insulinReq?.decimalValue,
  424. eventualBG: Int(truncating: lastDetermination.eventualBG ?? 0),
  425. sensitivityRatio: lastDetermination.sensitivityRatio?.decimalValue,
  426. rate: lastDetermination.rate?.decimalValue,
  427. duration: Int(lastDetermination.duration),
  428. iob: lastDetermination.iob?.decimalValue,
  429. cob: Decimal(lastDetermination.cob),
  430. predictions: nil,
  431. deliverAt: lastDetermination.deliverAt ?? Date(),
  432. carbsReq: Decimal(lastDetermination.carbsRequired),
  433. temp: TempType(rawValue: lastDetermination.temp ?? ""),
  434. bg: lastDetermination.glucose?.decimalValue,
  435. reservoir: lastDetermination.reservoir?.decimalValue,
  436. isf: lastDetermination.insulinSensitivity?.decimalValue,
  437. timestamp: lastDetermination.timestamp,
  438. recieved: lastDetermination.received,
  439. tdd: lastDetermination.totalDailyDose?.decimalValue ?? Decimal(0),
  440. insulin: Insulin(
  441. TDD: lastDetermination.totalDailyDose?.decimalValue ?? Decimal(0),
  442. bolus: lastDetermination.bolus?.decimalValue ?? Decimal(0),
  443. temp_basal: lastDetermination.tempBasal?.decimalValue ?? Decimal(0),
  444. scheduled_basal: lastDetermination.scheduledBasal?.decimalValue ?? Decimal(0)
  445. ),
  446. current_target: lastDetermination.currentTarget?.decimalValue ?? Decimal(0),
  447. insulinForManualBolus: lastDetermination.insulinForManualBolus?.decimalValue ?? Decimal(0),
  448. manualBolusErrorString: lastDetermination.manualBolusErrorString?.decimalValue ?? Decimal(0),
  449. minDelta: lastDetermination.minDelta?.decimalValue ?? Decimal(0),
  450. expectedDelta: lastDetermination.expectedDelta?.decimalValue ?? Decimal(0),
  451. minGuardBG: nil, minPredBG: nil, threshold: lastDetermination.threshold?.decimalValue ?? Decimal(0),
  452. carbRatio: lastDetermination.carbRatio?.decimalValue ?? Decimal(0)
  453. )
  454. } else if !lastDetermination.enacted, penultimateDetermination.enacted {
  455. suggested = Determination(
  456. reason: lastDetermination.reason ?? "",
  457. units: lastDetermination.smbToDeliver?.decimalValue,
  458. insulinReq: lastDetermination.insulinReq?.decimalValue,
  459. eventualBG: Int(truncating: lastDetermination.eventualBG ?? 0),
  460. sensitivityRatio: lastDetermination.sensitivityRatio?.decimalValue,
  461. rate: lastDetermination.rate?.decimalValue,
  462. duration: Int(lastDetermination.duration),
  463. iob: lastDetermination.iob?.decimalValue,
  464. cob: Decimal(lastDetermination.cob),
  465. predictions: nil,
  466. deliverAt: lastDetermination.deliverAt ?? Date(),
  467. carbsReq: Decimal(lastDetermination.carbsRequired),
  468. temp: TempType(rawValue: lastDetermination.temp ?? ""),
  469. bg: lastDetermination.glucose?.decimalValue,
  470. reservoir: lastDetermination.reservoir?.decimalValue,
  471. isf: lastDetermination.insulinSensitivity?.decimalValue,
  472. timestamp: lastDetermination.timestamp,
  473. recieved: lastDetermination.received,
  474. tdd: lastDetermination.totalDailyDose?.decimalValue ?? Decimal(0),
  475. insulin: Insulin(
  476. TDD: lastDetermination.totalDailyDose?.decimalValue ?? Decimal(0),
  477. bolus: lastDetermination.bolus?.decimalValue ?? Decimal(0),
  478. temp_basal: lastDetermination.tempBasal?.decimalValue ?? Decimal(0),
  479. scheduled_basal: lastDetermination.scheduledBasal?.decimalValue ?? Decimal(0)
  480. ),
  481. current_target: lastDetermination.currentTarget?.decimalValue ?? Decimal(0),
  482. insulinForManualBolus: lastDetermination.insulinForManualBolus?.decimalValue ?? Decimal(0),
  483. manualBolusErrorString: lastDetermination.manualBolusErrorString?.decimalValue ?? Decimal(0),
  484. minDelta: lastDetermination.minDelta?.decimalValue ?? Decimal(0),
  485. expectedDelta: lastDetermination.expectedDelta?.decimalValue ?? Decimal(0),
  486. minGuardBG: nil, minPredBG: nil, threshold: lastDetermination.threshold?.decimalValue ?? Decimal(0),
  487. carbRatio: lastDetermination.carbRatio?.decimalValue ?? Decimal(0)
  488. )
  489. enacted = Determination(
  490. reason: penultimateDetermination.reason ?? "",
  491. units: penultimateDetermination.smbToDeliver?.decimalValue,
  492. insulinReq: penultimateDetermination.insulinReq?.decimalValue,
  493. eventualBG: Int(truncating: penultimateDetermination.eventualBG ?? 0),
  494. sensitivityRatio: penultimateDetermination.sensitivityRatio?.decimalValue,
  495. rate: penultimateDetermination.rate?.decimalValue,
  496. duration: Int(penultimateDetermination.duration),
  497. iob: penultimateDetermination.iob?.decimalValue,
  498. cob: Decimal(penultimateDetermination.cob),
  499. predictions: nil,
  500. deliverAt: penultimateDetermination.deliverAt ?? Date(),
  501. carbsReq: Decimal(penultimateDetermination.carbsRequired),
  502. temp: TempType(rawValue: penultimateDetermination.temp ?? ""),
  503. bg: penultimateDetermination.glucose?.decimalValue,
  504. reservoir: penultimateDetermination.reservoir?.decimalValue,
  505. isf: penultimateDetermination.insulinSensitivity?.decimalValue,
  506. timestamp: penultimateDetermination.timestamp,
  507. recieved: penultimateDetermination.received,
  508. tdd: penultimateDetermination.totalDailyDose?.decimalValue ?? Decimal(0),
  509. insulin: Insulin(
  510. TDD: penultimateDetermination.totalDailyDose?.decimalValue ?? Decimal(0),
  511. bolus: penultimateDetermination.bolus?.decimalValue ?? Decimal(0),
  512. temp_basal: penultimateDetermination.tempBasal?.decimalValue ?? Decimal(0),
  513. scheduled_basal: penultimateDetermination.scheduledBasal?.decimalValue ?? Decimal(0)
  514. ),
  515. current_target: penultimateDetermination.currentTarget?.decimalValue ?? Decimal(0),
  516. insulinForManualBolus: penultimateDetermination.insulinForManualBolus?.decimalValue ?? Decimal(0),
  517. manualBolusErrorString: penultimateDetermination.manualBolusErrorString?.decimalValue ?? Decimal(0),
  518. minDelta: penultimateDetermination.minDelta?.decimalValue ?? Decimal(0),
  519. expectedDelta: penultimateDetermination.expectedDelta?.decimalValue ?? Decimal(0),
  520. minGuardBG: nil,
  521. minPredBG: nil,
  522. threshold: penultimateDetermination.threshold?.decimalValue ?? Decimal(0),
  523. carbRatio: penultimateDetermination.carbRatio?.decimalValue ?? Decimal(0)
  524. )
  525. } else if !lastDetermination.enacted, !penultimateDetermination.enacted {
  526. suggested = Determination(
  527. reason: lastDetermination.reason ?? "",
  528. units: lastDetermination.smbToDeliver?.decimalValue,
  529. insulinReq: lastDetermination.insulinReq?.decimalValue,
  530. eventualBG: Int(truncating: lastDetermination.eventualBG ?? 0),
  531. sensitivityRatio: lastDetermination.sensitivityRatio?.decimalValue,
  532. rate: lastDetermination.rate?.decimalValue,
  533. duration: Int(lastDetermination.duration),
  534. iob: lastDetermination.iob?.decimalValue,
  535. cob: Decimal(lastDetermination.cob),
  536. predictions: nil,
  537. deliverAt: lastDetermination.deliverAt ?? Date(),
  538. carbsReq: Decimal(lastDetermination.carbsRequired),
  539. temp: TempType(rawValue: lastDetermination.temp ?? ""),
  540. bg: lastDetermination.glucose?.decimalValue,
  541. reservoir: lastDetermination.reservoir?.decimalValue,
  542. isf: lastDetermination.insulinSensitivity?.decimalValue,
  543. timestamp: lastDetermination.timestamp,
  544. recieved: lastDetermination.received,
  545. tdd: lastDetermination.totalDailyDose?.decimalValue ?? Decimal(0),
  546. insulin: Insulin(
  547. TDD: lastDetermination.totalDailyDose?.decimalValue ?? Decimal(0),
  548. bolus: lastDetermination.bolus?.decimalValue ?? Decimal(0),
  549. temp_basal: lastDetermination.tempBasal?.decimalValue ?? Decimal(0),
  550. scheduled_basal: lastDetermination.scheduledBasal?.decimalValue ?? Decimal(0)
  551. ),
  552. current_target: lastDetermination.currentTarget?.decimalValue ?? Decimal(0),
  553. insulinForManualBolus: lastDetermination.insulinForManualBolus?.decimalValue ?? Decimal(0),
  554. manualBolusErrorString: lastDetermination.manualBolusErrorString?.decimalValue ?? Decimal(0),
  555. minDelta: lastDetermination.minDelta?.decimalValue ?? Decimal(0),
  556. expectedDelta: lastDetermination.expectedDelta?.decimalValue ?? Decimal(0),
  557. minGuardBG: nil, minPredBG: nil, threshold: lastDetermination.threshold?.decimalValue ?? Decimal(0),
  558. carbRatio: lastDetermination.carbRatio?.decimalValue ?? Decimal(0)
  559. )
  560. }
  561. }
  562. let loopIsClosed = settingsManager.settings.closedLoop
  563. var openapsStatus: OpenAPSStatus
  564. // Only upload suggested in Open Loop Mode. Only upload enacted in Closed Loop Mode.
  565. if loopIsClosed {
  566. openapsStatus = OpenAPSStatus(
  567. iob: iob?.first,
  568. suggested: nil,
  569. enacted: enacted,
  570. version: "0.7.1"
  571. )
  572. } else {
  573. openapsStatus = OpenAPSStatus(
  574. iob: iob?.first,
  575. suggested: suggested,
  576. enacted: nil,
  577. version: "0.7.1"
  578. )
  579. }
  580. let battery = fetchBattery()
  581. var reservoir = Decimal(from: storage.retrieveRaw(OpenAPS.Monitor.reservoir) ?? "0")
  582. if reservoir == 0xDEAD_BEEF {
  583. reservoir = nil
  584. }
  585. let pumpStatus = storage.retrieve(OpenAPS.Monitor.status, as: PumpStatus.self)
  586. let pump = NSPumpStatus(clock: Date(), battery: battery, reservoir: reservoir, status: pumpStatus)
  587. let device = UIDevice.current
  588. let uploader = Uploader(batteryVoltage: nil, battery: Int(device.batteryLevel * 100))
  589. var status: NightscoutStatus
  590. status = NightscoutStatus(
  591. device: NigtscoutTreatment.local,
  592. openaps: openapsStatus,
  593. pump: pump,
  594. uploader: uploader
  595. )
  596. storage.save(status, as: OpenAPS.Upload.nsStatus)
  597. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  598. return
  599. }
  600. processQueue.async {
  601. nightscout.uploadStatus(status)
  602. .sink { completion in
  603. switch completion {
  604. case .finished:
  605. debug(.nightscout, "Status uploaded")
  606. case let .failure(error):
  607. debug(.nightscout, error.localizedDescription)
  608. }
  609. } receiveValue: {}
  610. .store(in: &self.lifetime)
  611. }
  612. uploadPodAge()
  613. }
  614. func uploadPodAge() {
  615. let uploadedPodAge = storage.retrieve(OpenAPS.Nightscout.uploadedPodAge, as: [NigtscoutTreatment].self) ?? []
  616. if let podAge = storage.retrieve(OpenAPS.Monitor.podAge, as: Date.self),
  617. uploadedPodAge.last?.createdAt == nil || podAge != uploadedPodAge.last!.createdAt!
  618. {
  619. let siteTreatment = NigtscoutTreatment(
  620. duration: nil,
  621. rawDuration: nil,
  622. rawRate: nil,
  623. absolute: nil,
  624. rate: nil,
  625. eventType: .nsSiteChange,
  626. createdAt: podAge,
  627. enteredBy: NigtscoutTreatment.local,
  628. bolus: nil,
  629. insulin: nil,
  630. notes: nil,
  631. carbs: nil,
  632. fat: nil,
  633. protein: nil,
  634. targetTop: nil,
  635. targetBottom: nil
  636. )
  637. uploadTreatments([siteTreatment], fileToSave: OpenAPS.Nightscout.uploadedPodAge)
  638. }
  639. }
  640. func uploadProfileAndSettings(_ force: Bool) {
  641. guard let sensitivities = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self) else {
  642. debug(.nightscout, "NightscoutManager uploadProfile: error loading insulinSensitivities")
  643. return
  644. }
  645. guard let settings = storage.retrieve(OpenAPS.FreeAPS.settings, as: FreeAPSSettings.self) else {
  646. debug(.nightscout, "NightscoutManager uploadProfile: error loading settings")
  647. return
  648. }
  649. guard let preferences = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) else {
  650. debug(.nightscout, "NightscoutManager uploadProfile: error loading preferences")
  651. return
  652. }
  653. guard let targets = storage.retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self) else {
  654. debug(.nightscout, "NightscoutManager uploadProfile: error loading bgTargets")
  655. return
  656. }
  657. guard let carbRatios = storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self) else {
  658. debug(.nightscout, "NightscoutManager uploadProfile: error loading carbRatios")
  659. return
  660. }
  661. guard let basalProfile = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self) else {
  662. debug(.nightscout, "NightscoutManager uploadProfile: error loading basalProfile")
  663. return
  664. }
  665. let sens = sensitivities.sensitivities.map { item -> NightscoutTimevalue in
  666. NightscoutTimevalue(
  667. time: String(item.start.prefix(5)),
  668. value: item.sensitivity,
  669. timeAsSeconds: item.offset * 60
  670. )
  671. }
  672. let target_low = targets.targets.map { item -> NightscoutTimevalue in
  673. NightscoutTimevalue(
  674. time: String(item.start.prefix(5)),
  675. value: item.low,
  676. timeAsSeconds: item.offset * 60
  677. )
  678. }
  679. let target_high = targets.targets.map { item -> NightscoutTimevalue in
  680. NightscoutTimevalue(
  681. time: String(item.start.prefix(5)),
  682. value: item.high,
  683. timeAsSeconds: item.offset * 60
  684. )
  685. }
  686. let cr = carbRatios.schedule.map { item -> NightscoutTimevalue in
  687. NightscoutTimevalue(
  688. time: String(item.start.prefix(5)),
  689. value: item.ratio,
  690. timeAsSeconds: item.offset * 60
  691. )
  692. }
  693. let basal = basalProfile.map { item -> NightscoutTimevalue in
  694. NightscoutTimevalue(
  695. time: String(item.start.prefix(5)),
  696. value: item.rate,
  697. timeAsSeconds: item.minutes * 60
  698. )
  699. }
  700. var nsUnits = ""
  701. switch settingsManager.settings.units {
  702. case .mgdL:
  703. nsUnits = "mg/dl"
  704. case .mmolL:
  705. nsUnits = "mmol"
  706. }
  707. var carbs_hr: Decimal = 0
  708. if let isf = sensitivities.sensitivities.map(\.sensitivity).first,
  709. let cr = carbRatios.schedule.map(\.ratio).first,
  710. isf > 0, cr > 0
  711. {
  712. // CarbImpact -> Carbs/hr = CI [mg/dl/5min] * 12 / ISF [mg/dl/U] * CR [g/U]
  713. carbs_hr = settingsManager.preferences.min5mCarbimpact * 12 / isf * cr
  714. if settingsManager.settings.units == .mmolL {
  715. carbs_hr = carbs_hr * GlucoseUnits.exchangeRate
  716. }
  717. // No, Decimal has no rounding function.
  718. carbs_hr = Decimal(round(Double(carbs_hr) * 10.0)) / 10
  719. }
  720. let ps = ScheduledNightscoutProfile(
  721. dia: settingsManager.pumpSettings.insulinActionCurve,
  722. carbs_hr: Int(carbs_hr),
  723. delay: 0,
  724. timezone: TimeZone.current.identifier,
  725. target_low: target_low,
  726. target_high: target_high,
  727. sens: sens,
  728. basal: basal,
  729. carbratio: cr,
  730. units: nsUnits
  731. )
  732. let defaultProfile = "default"
  733. let now = Date()
  734. let p = NightscoutProfileStore(
  735. defaultProfile: defaultProfile,
  736. startDate: now,
  737. mills: Int(now.timeIntervalSince1970) * 1000,
  738. units: nsUnits,
  739. enteredBy: NigtscoutTreatment.local,
  740. store: [defaultProfile: ps]
  741. )
  742. guard let nightscout = nightscoutAPI, isNetworkReachable, isUploadEnabled else {
  743. return
  744. }
  745. // UPLOAD PREFERNCES WHEN CHANGED
  746. if let uploadedPreferences = storage.retrieve(OpenAPS.Nightscout.uploadedPreferences, as: Preferences.self),
  747. uploadedPreferences.rawJSON.sorted() == preferences.rawJSON.sorted(), !force
  748. {
  749. NSLog("NightscoutManager Preferences, preferences unchanged")
  750. } else { uploadPreferences(preferences) }
  751. // UPLOAD FreeAPS Settings WHEN CHANGED
  752. if let uploadedSettings = storage.retrieve(OpenAPS.Nightscout.uploadedSettings, as: FreeAPSSettings.self),
  753. uploadedSettings.rawJSON.sorted() == settings.rawJSON.sorted(), !force
  754. {
  755. NSLog("NightscoutManager Settings, settings unchanged")
  756. } else { uploadSettings(settings) }
  757. // UPLOAD Profiles WHEN CHANGED
  758. if let uploadedProfile = storage.retrieve(OpenAPS.Nightscout.uploadedProfile, as: NightscoutProfileStore.self),
  759. (uploadedProfile.store["default"]?.rawJSON ?? "").sorted() == ps.rawJSON.sorted(), !force
  760. {
  761. NSLog("NightscoutManager uploadProfile, no profile change")
  762. } else {
  763. processQueue.async {
  764. nightscout.uploadProfile(p)
  765. .sink { completion in
  766. switch completion {
  767. case .finished:
  768. self.storage.save(p, as: OpenAPS.Nightscout.uploadedProfile)
  769. debug(.nightscout, "Profile uploaded")
  770. case let .failure(error):
  771. debug(.nightscout, error.localizedDescription)
  772. }
  773. } receiveValue: {}
  774. .store(in: &self.lifetime)
  775. }
  776. }
  777. }
  778. func uploadGlucose() {
  779. uploadGlucose(glucoseStorage.nightscoutGlucoseNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedGlucose)
  780. uploadTreatments(glucoseStorage.nightscoutCGMStateNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCGMState)
  781. }
  782. func uploadManualGlucose() {
  783. uploadTreatments(
  784. glucoseStorage.nightscoutManualGlucoseNotUploaded(),
  785. fileToSave: OpenAPS.Nightscout.uploadedManualGlucose
  786. )
  787. }
  788. private func uploadPumpHistory() {
  789. uploadTreatments(pumpHistoryStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedPumphistory)
  790. }
  791. private func uploadCarbs() {
  792. uploadTreatments(carbsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCarbs)
  793. }
  794. private func uploadTempTargets() {
  795. uploadTreatments(tempTargetsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedTempTargets)
  796. }
  797. private func uploadGlucose(_ glucose: [BloodGlucose], fileToSave: String) {
  798. guard !glucose.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled, isUploadGlucoseEnabled else {
  799. return
  800. }
  801. // check if unique code
  802. // var uuid = UUID(uuidString: yourString) This will return nil if yourString is not a valid UUID
  803. let glucoseWithoutCorrectID = glucose.filter { UUID(uuidString: $0._id) != nil }
  804. processQueue.async {
  805. glucoseWithoutCorrectID.chunks(ofCount: 100)
  806. .map { chunk -> AnyPublisher<Void, Error> in
  807. nightscout.uploadGlucose(Array(chunk))
  808. }
  809. .reduce(
  810. Just(()).setFailureType(to: Error.self)
  811. .eraseToAnyPublisher()
  812. ) { (result, next) -> AnyPublisher<Void, Error> in
  813. Publishers.Concatenate(prefix: result, suffix: next).eraseToAnyPublisher()
  814. }
  815. .dropFirst()
  816. .sink { completion in
  817. switch completion {
  818. case .finished:
  819. self.storage.save(glucose, as: fileToSave)
  820. debug(.nightscout, "Glucose uploaded")
  821. case let .failure(error):
  822. debug(.nightscout, "Upload of glucose failed: " + error.localizedDescription)
  823. }
  824. } receiveValue: {}
  825. .store(in: &self.lifetime)
  826. }
  827. }
  828. private func uploadTreatments(_ treatments: [NigtscoutTreatment], fileToSave: String) {
  829. guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
  830. return
  831. }
  832. processQueue.async {
  833. treatments.chunks(ofCount: 100)
  834. .map { chunk -> AnyPublisher<Void, Error> in
  835. nightscout.uploadTreatments(Array(chunk))
  836. }
  837. .reduce(
  838. Just(()).setFailureType(to: Error.self)
  839. .eraseToAnyPublisher()
  840. ) { (result, next) -> AnyPublisher<Void, Error> in
  841. Publishers.Concatenate(prefix: result, suffix: next).eraseToAnyPublisher()
  842. }
  843. .dropFirst()
  844. .sink { completion in
  845. switch completion {
  846. case .finished:
  847. self.storage.save(treatments, as: fileToSave)
  848. debug(.nightscout, "Treatments uploaded")
  849. case let .failure(error):
  850. debug(.nightscout, error.localizedDescription)
  851. }
  852. } receiveValue: {}
  853. .store(in: &self.lifetime)
  854. }
  855. }
  856. }
  857. extension BaseNightscoutManager: PumpHistoryObserver {
  858. func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
  859. uploadPumpHistory()
  860. }
  861. }
  862. extension BaseNightscoutManager: CarbsObserver {
  863. func carbsDidUpdate(_: [CarbsEntry]) {
  864. uploadCarbs()
  865. }
  866. }
  867. extension BaseNightscoutManager: TempTargetsObserver {
  868. func tempTargetsDidUpdate(_: [TempTarget]) {
  869. uploadTempTargets()
  870. }
  871. }
  872. extension BaseNightscoutManager: GlucoseObserver {
  873. func glucoseDidUpdate(_: [BloodGlucose]) {
  874. uploadManualGlucose()
  875. }
  876. }