NightscoutManager.swift 18 KB

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