NightscoutManager.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. import Combine
  2. import Foundation
  3. import Swinject
  4. import UIKit
  5. protocol NightscoutManager: GlucoseSource {
  6. func fetchGlucose(since date: Date) -> AnyPublisher<[BloodGlucose], Never>
  7. func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never>
  8. func fetchTempTargets() -> AnyPublisher<[TempTarget], Never>
  9. func fetchAnnouncements() -> AnyPublisher<[Announcement], Never>
  10. func deleteCarbs(at date: Date)
  11. func uploadStatus()
  12. func uploadStatistics(dailystat: Statistics)
  13. func uploadPreferences()
  14. func uploadGlucose()
  15. func uploadProfile()
  16. var cgmURL: URL? { get }
  17. }
  18. final class BaseNightscoutManager: NightscoutManager, Injectable {
  19. @Injected() private var keychain: Keychain!
  20. @Injected() private var glucoseStorage: GlucoseStorage!
  21. @Injected() private var tempTargetsStorage: TempTargetsStorage!
  22. @Injected() private var carbsStorage: CarbsStorage!
  23. @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
  24. @Injected() private var storage: FileStorage!
  25. @Injected() private var announcementsStorage: AnnouncementsStorage!
  26. @Injected() private var settingsManager: SettingsManager!
  27. @Injected() private var broadcaster: Broadcaster!
  28. @Injected() private var reachabilityManager: ReachabilityManager!
  29. private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
  30. private var ping: TimeInterval?
  31. private var lifetime = Lifetime()
  32. private var isNetworkReachable: Bool {
  33. reachabilityManager.isReachable
  34. }
  35. private var isUploadEnabled: Bool {
  36. settingsManager.settings.isUploadEnabled
  37. }
  38. private var isUploadGlucoseEnabled: Bool {
  39. settingsManager.settings.uploadGlucose
  40. }
  41. private var nightscoutAPI: NightscoutAPI? {
  42. guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
  43. let url = URL(string: urlString),
  44. let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
  45. else {
  46. return nil
  47. }
  48. return NightscoutAPI(url: url, secret: secret)
  49. }
  50. init(resolver: Resolver) {
  51. injectServices(resolver)
  52. subscribe()
  53. }
  54. private func subscribe() {
  55. broadcaster.register(PumpHistoryObserver.self, observer: self)
  56. broadcaster.register(CarbsObserver.self, observer: self)
  57. broadcaster.register(TempTargetsObserver.self, observer: self)
  58. _ = reachabilityManager.startListening(onQueue: processQueue) { status in
  59. debug(.nightscout, "Network status: \(status)")
  60. }
  61. }
  62. func sourceInfo() -> [String: Any]? {
  63. if let ping = ping {
  64. return [GlucoseSourceKey.nightscoutPing.rawValue: ping]
  65. }
  66. return nil
  67. }
  68. var cgmURL: URL? {
  69. if let url = settingsManager.settings.cgm.appURL {
  70. return url
  71. }
  72. let useLocal = settingsManager.settings.useLocalGlucoseSource
  73. let maybeNightscout = useLocal
  74. ? NightscoutAPI(url: URL(string: "http://127.0.0.1:\(settingsManager.settings.localGlucosePort)")!)
  75. : nightscoutAPI
  76. return maybeNightscout?.url
  77. }
  78. func fetchGlucose(since date: Date) -> AnyPublisher<[BloodGlucose], Never> {
  79. let useLocal = settingsManager.settings.useLocalGlucoseSource
  80. ping = nil
  81. if !useLocal {
  82. guard isNetworkReachable else {
  83. return Just([]).eraseToAnyPublisher()
  84. }
  85. }
  86. let maybeNightscout = useLocal
  87. ? NightscoutAPI(url: URL(string: "http://127.0.0.1:\(settingsManager.settings.localGlucosePort)")!)
  88. : nightscoutAPI
  89. guard let nightscout = maybeNightscout else {
  90. return Just([]).eraseToAnyPublisher()
  91. }
  92. let startDate = Date()
  93. return nightscout.fetchLastGlucose(sinceDate: date)
  94. .tryCatch({ (error) -> AnyPublisher<[BloodGlucose], Error> in
  95. print(error.localizedDescription)
  96. return Just([]).setFailureType(to: Error.self).eraseToAnyPublisher()
  97. })
  98. .replaceError(with: [])
  99. .handleEvents(receiveOutput: { value in
  100. guard value.isNotEmpty else { return }
  101. self.ping = Date().timeIntervalSince(startDate)
  102. })
  103. .eraseToAnyPublisher()
  104. }
  105. func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
  106. fetchGlucose(since: glucoseStorage.syncDate())
  107. }
  108. func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never> {
  109. guard let nightscout = nightscoutAPI, isNetworkReachable else {
  110. return Just([]).eraseToAnyPublisher()
  111. }
  112. let since = carbsStorage.syncDate()
  113. return nightscout.fetchCarbs(sinceDate: since)
  114. .replaceError(with: [])
  115. .eraseToAnyPublisher()
  116. }
  117. func fetchTempTargets() -> AnyPublisher<[TempTarget], Never> {
  118. guard let nightscout = nightscoutAPI, isNetworkReachable else {
  119. return Just([]).eraseToAnyPublisher()
  120. }
  121. let since = tempTargetsStorage.syncDate()
  122. return nightscout.fetchTempTargets(sinceDate: since)
  123. .replaceError(with: [])
  124. .eraseToAnyPublisher()
  125. }
  126. func fetchAnnouncements() -> AnyPublisher<[Announcement], Never> {
  127. guard let nightscout = nightscoutAPI, isNetworkReachable else {
  128. return Just([]).eraseToAnyPublisher()
  129. }
  130. let since = announcementsStorage.syncDate()
  131. return nightscout.fetchAnnouncement(sinceDate: since)
  132. .replaceError(with: [])
  133. .eraseToAnyPublisher()
  134. }
  135. func deleteCarbs(at date: Date) {
  136. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  137. carbsStorage.deleteCarbs(at: date)
  138. return
  139. }
  140. nightscout.deleteCarbs(at: date)
  141. .sink { completion in
  142. switch completion {
  143. case .finished:
  144. self.carbsStorage.deleteCarbs(at: date)
  145. debug(.nightscout, "Carbs deleted")
  146. case let .failure(error):
  147. debug(.nightscout, error.localizedDescription)
  148. }
  149. } receiveValue: {}
  150. .store(in: &lifetime)
  151. }
  152. func uploadStatistics(dailystat: Statistics) {
  153. let stats = NightscoutStatistics(
  154. dailystats: dailystat
  155. )
  156. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  157. return
  158. }
  159. processQueue.async {
  160. nightscout.uploadStats(stats)
  161. .sink { completion in
  162. switch completion {
  163. case .finished:
  164. debug(.nightscout, "Statistics uploaded")
  165. case let .failure(error):
  166. debug(.nightscout, error.localizedDescription)
  167. }
  168. } receiveValue: {}
  169. .store(in: &self.lifetime)
  170. }
  171. }
  172. func uploadPreferences() {
  173. let prefs = NightscoutPreferences(
  174. preferences: settingsManager.preferences
  175. )
  176. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  177. return
  178. }
  179. processQueue.async {
  180. nightscout.uploadPrefs(prefs)
  181. .sink { completion in
  182. switch completion {
  183. case .finished:
  184. debug(.nightscout, "Preferences uploaded")
  185. case let .failure(error):
  186. debug(.nightscout, error.localizedDescription)
  187. }
  188. } receiveValue: {}
  189. .store(in: &self.lifetime)
  190. }
  191. }
  192. func uploadStatus() {
  193. let iob = storage.retrieve(OpenAPS.Monitor.iob, as: [IOBEntry].self)
  194. var suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self)
  195. var enacted = storage.retrieve(OpenAPS.Enact.enacted, as: Suggestion.self)
  196. if (suggested?.timestamp ?? .distantPast) > (enacted?.timestamp ?? .distantPast) {
  197. enacted?.predictions = nil
  198. } else {
  199. suggested?.predictions = nil
  200. }
  201. let loopIsClosed = settingsManager.settings.closedLoop
  202. var openapsStatus: OpenAPSStatus
  203. // Only upload suggested in Open Loop Mode. Only upload enacted in Closed Loop Mode.
  204. if loopIsClosed {
  205. openapsStatus = OpenAPSStatus(
  206. iob: iob?.first,
  207. suggested: nil,
  208. enacted: enacted,
  209. version: "0.7.1"
  210. )
  211. } else {
  212. openapsStatus = OpenAPSStatus(
  213. iob: iob?.first,
  214. suggested: suggested,
  215. enacted: nil,
  216. version: "0.7.1"
  217. )
  218. }
  219. let battery = storage.retrieve(OpenAPS.Monitor.battery, as: Battery.self)
  220. var reservoir = Decimal(from: storage.retrieveRaw(OpenAPS.Monitor.reservoir) ?? "0")
  221. if reservoir == 0xDEAD_BEEF {
  222. reservoir = nil
  223. }
  224. let pumpStatus = storage.retrieve(OpenAPS.Monitor.status, as: PumpStatus.self)
  225. let pump = NSPumpStatus(clock: Date(), battery: battery, reservoir: reservoir, status: pumpStatus)
  226. let device = UIDevice.current
  227. let uploader = Uploader(batteryVoltage: nil, battery: Int(device.batteryLevel * 100))
  228. var status: NightscoutStatus
  229. status = NightscoutStatus(
  230. device: NigtscoutTreatment.local,
  231. openaps: openapsStatus,
  232. pump: pump,
  233. uploader: uploader
  234. )
  235. storage.save(status, as: OpenAPS.Upload.nsStatus)
  236. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  237. return
  238. }
  239. processQueue.async {
  240. nightscout.uploadStatus(status)
  241. .sink { completion in
  242. switch completion {
  243. case .finished:
  244. debug(.nightscout, "Status uploaded")
  245. case let .failure(error):
  246. debug(.nightscout, error.localizedDescription)
  247. }
  248. } receiveValue: {}
  249. .store(in: &self.lifetime)
  250. }
  251. uploadPodAge()
  252. }
  253. func uploadPodAge() {
  254. let uploadedPodAge = storage.retrieve(OpenAPS.Nightscout.uploadedPodAge, as: [NigtscoutTreatment].self) ?? []
  255. if let podAge = storage.retrieve(OpenAPS.Monitor.podAge, as: Date.self),
  256. uploadedPodAge.last?.createdAt == nil || podAge != uploadedPodAge.last!.createdAt!
  257. {
  258. let siteTreatment = NigtscoutTreatment(
  259. duration: nil,
  260. rawDuration: nil,
  261. rawRate: nil,
  262. absolute: nil,
  263. rate: nil,
  264. eventType: .nsSiteChange,
  265. createdAt: podAge,
  266. enteredBy: NigtscoutTreatment.local,
  267. bolus: nil,
  268. insulin: nil,
  269. notes: nil,
  270. carbs: nil,
  271. targetTop: nil,
  272. targetBottom: nil
  273. )
  274. uploadTreatments([siteTreatment], fileToSave: OpenAPS.Nightscout.uploadedPodAge)
  275. }
  276. }
  277. func uploadProfile() {
  278. // These should be modified anyways and not the defaults
  279. guard let sensitivities = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self),
  280. let basalProfile = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self),
  281. let carbRatios = storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self),
  282. let targets = storage.retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self)
  283. else {
  284. NSLog("NightscoutManager uploadProfile Not all settings found to build profile!")
  285. return
  286. }
  287. let sens = sensitivities.sensitivities.map { item -> NightscoutTimevalue in
  288. NightscoutTimevalue(
  289. time: String(item.start.prefix(5)),
  290. value: item.sensitivity,
  291. timeAsSeconds: item.offset
  292. )
  293. }
  294. let target_low = targets.targets.map { item -> NightscoutTimevalue in
  295. NightscoutTimevalue(
  296. time: String(item.start.prefix(5)),
  297. value: item.low,
  298. timeAsSeconds: item.offset
  299. )
  300. }
  301. let target_high = targets.targets.map { item -> NightscoutTimevalue in
  302. NightscoutTimevalue(
  303. time: String(item.start.prefix(5)),
  304. value: item.high,
  305. timeAsSeconds: item.offset
  306. )
  307. }
  308. let cr = carbRatios.schedule.map { item -> NightscoutTimevalue in
  309. NightscoutTimevalue(
  310. time: String(item.start.prefix(5)),
  311. value: item.ratio,
  312. timeAsSeconds: item.offset
  313. )
  314. }
  315. let basal = basalProfile.map { item -> NightscoutTimevalue in
  316. NightscoutTimevalue(
  317. time: String(item.start.prefix(5)),
  318. value: item.rate,
  319. timeAsSeconds: item.minutes * 60
  320. )
  321. }
  322. var nsUnits = ""
  323. switch settingsManager.settings.units {
  324. case .mgdL:
  325. nsUnits = "mg/dl"
  326. case .mmolL:
  327. nsUnits = "mmol"
  328. }
  329. var carbs_hr: Decimal = 0
  330. if let isf = sensitivities.sensitivities.map(\.sensitivity).first,
  331. let cr = carbRatios.schedule.map(\.ratio).first,
  332. isf > 0, cr > 0
  333. {
  334. // CarbImpact -> Carbs/hr = CI [mg/dl/5min] * 12 / ISF [mg/dl/U] * CR [g/U]
  335. carbs_hr = settingsManager.preferences.min5mCarbimpact * 12 / isf * cr
  336. if settingsManager.settings.units == .mmolL {
  337. carbs_hr = carbs_hr * GlucoseUnits.exchangeRate
  338. }
  339. // No, Decimal has no rounding function.
  340. carbs_hr = Decimal(round(Double(carbs_hr) * 10.0)) / 10
  341. }
  342. let ps = ScheduledNightscoutProfile(
  343. dia: settingsManager.pumpSettings.insulinActionCurve,
  344. carbs_hr: Int(carbs_hr),
  345. delay: 0,
  346. timezone: TimeZone.current.identifier,
  347. target_low: target_low,
  348. target_high: target_high,
  349. sens: sens,
  350. basal: basal,
  351. carbratio: cr,
  352. units: nsUnits
  353. )
  354. let defaultProfile = "default"
  355. let now = Date()
  356. let p = NightscoutProfileStore(
  357. defaultProfile: defaultProfile,
  358. startDate: now,
  359. mills: Int(now.timeIntervalSince1970) * 1000,
  360. units: nsUnits,
  361. enteredBy: NigtscoutTreatment.local,
  362. store: [defaultProfile: ps]
  363. )
  364. if let uploadedProfile = storage.retrieve(OpenAPS.Nightscout.uploadedProfile, as: NightscoutProfileStore.self),
  365. (uploadedProfile.store[defaultProfile]?.rawJSON ?? "") == ps.rawJSON
  366. {
  367. NSLog("NightscoutManager uploadProfile, no profile change")
  368. return
  369. }
  370. guard let nightscout = nightscoutAPI, isNetworkReachable, isUploadEnabled else {
  371. return
  372. }
  373. processQueue.async {
  374. nightscout.uploadProfile(p)
  375. .sink { completion in
  376. switch completion {
  377. case .finished:
  378. self.storage.save(p, as: OpenAPS.Nightscout.uploadedProfile)
  379. debug(.nightscout, "Profile uploaded")
  380. case let .failure(error):
  381. debug(.nightscout, error.localizedDescription)
  382. }
  383. } receiveValue: {}
  384. .store(in: &self.lifetime)
  385. }
  386. }
  387. func uploadGlucose() {
  388. uploadGlucose(glucoseStorage.nightscoutGlucoseNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedGlucose)
  389. uploadTreatments(glucoseStorage.nightscoutCGMStateNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCGMState)
  390. }
  391. private func uploadPumpHistory() {
  392. uploadTreatments(pumpHistoryStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedPumphistory)
  393. }
  394. private func uploadCarbs() {
  395. uploadTreatments(carbsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCarbs)
  396. }
  397. private func uploadTempTargets() {
  398. uploadTreatments(tempTargetsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedTempTargets)
  399. }
  400. private func uploadGlucose(_ glucose: [BloodGlucose], fileToSave: String) {
  401. guard !glucose.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled, isUploadGlucoseEnabled else {
  402. return
  403. }
  404. processQueue.async {
  405. glucose.chunks(ofCount: 100)
  406. .map { chunk -> AnyPublisher<Void, Error> in
  407. nightscout.uploadGlucose(Array(chunk))
  408. }
  409. .reduce(
  410. Just(()).setFailureType(to: Error.self)
  411. .eraseToAnyPublisher()
  412. ) { (result, next) -> AnyPublisher<Void, Error> in
  413. Publishers.Concatenate(prefix: result, suffix: next).eraseToAnyPublisher()
  414. }
  415. .dropFirst()
  416. .sink { completion in
  417. switch completion {
  418. case .finished:
  419. self.storage.save(glucose, as: fileToSave)
  420. case let .failure(error):
  421. debug(.nightscout, error.localizedDescription)
  422. }
  423. } receiveValue: {}
  424. .store(in: &self.lifetime)
  425. }
  426. }
  427. private func uploadTreatments(_ treatments: [NigtscoutTreatment], fileToSave: String) {
  428. guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
  429. return
  430. }
  431. processQueue.async {
  432. treatments.chunks(ofCount: 100)
  433. .map { chunk -> AnyPublisher<Void, Error> in
  434. nightscout.uploadTreatments(Array(chunk))
  435. }
  436. .reduce(
  437. Just(()).setFailureType(to: Error.self)
  438. .eraseToAnyPublisher()
  439. ) { (result, next) -> AnyPublisher<Void, Error> in
  440. Publishers.Concatenate(prefix: result, suffix: next).eraseToAnyPublisher()
  441. }
  442. .dropFirst()
  443. .sink { completion in
  444. switch completion {
  445. case .finished:
  446. self.storage.save(treatments, as: fileToSave)
  447. case let .failure(error):
  448. debug(.nightscout, error.localizedDescription)
  449. }
  450. } receiveValue: {}
  451. .store(in: &self.lifetime)
  452. }
  453. }
  454. }
  455. extension BaseNightscoutManager: PumpHistoryObserver {
  456. func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
  457. uploadPumpHistory()
  458. }
  459. }
  460. extension BaseNightscoutManager: CarbsObserver {
  461. func carbsDidUpdate(_: [CarbsEntry]) {
  462. uploadCarbs()
  463. }
  464. }
  465. extension BaseNightscoutManager: TempTargetsObserver {
  466. func tempTargetsDidUpdate(_: [TempTarget]) {
  467. uploadTempTargets()
  468. }
  469. }