NightscoutManager.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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() -> 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 status = NightscoutStatus(
  176. device: "freeaps-x://" + device.name,
  177. openaps: openapsStatus,
  178. pump: pump,
  179. preferences: preferences,
  180. uploader: uploader
  181. )
  182. storage.save(status, as: OpenAPS.Upload.nsStatus)
  183. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  184. return
  185. }
  186. processQueue.async {
  187. nightscout.uploadStatus(status)
  188. .sink { completion in
  189. switch completion {
  190. case .finished:
  191. debug(.nightscout, "Status uploaded")
  192. case let .failure(error):
  193. debug(.nightscout, error.localizedDescription)
  194. }
  195. } receiveValue: {}
  196. .store(in: &self.lifetime)
  197. }
  198. uploadPodAge()
  199. }
  200. func uploadPodAge() {
  201. let uploadedPodAge = storage.retrieve(OpenAPS.Nightscout.uploadedPodAge, as: [NigtscoutTreatment].self) ?? []
  202. if let podAge = storage.retrieve(OpenAPS.Monitor.podAge, as: Date.self),
  203. uploadedPodAge.last?.createdAt == nil || podAge != uploadedPodAge.last!.createdAt!
  204. {
  205. let siteTreatment = NigtscoutTreatment(
  206. duration: nil,
  207. rawDuration: nil,
  208. rawRate: nil,
  209. absolute: nil,
  210. rate: nil,
  211. eventType: .nsSiteChange,
  212. createdAt: podAge,
  213. enteredBy: NigtscoutTreatment.local,
  214. bolus: nil,
  215. insulin: nil,
  216. notes: nil,
  217. carbs: nil,
  218. targetTop: nil,
  219. targetBottom: nil
  220. )
  221. uploadTreatments([siteTreatment], fileToSave: OpenAPS.Nightscout.uploadedPodAge)
  222. }
  223. }
  224. func uploadProfile() {
  225. // These should be modified anyways and not the defaults
  226. guard let sensitivities = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self),
  227. let basalProfile = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self),
  228. let carbRatios = storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self),
  229. let targets = storage.retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self)
  230. else {
  231. NSLog("NightscoutManager uploadProfile Not all settings found to build profile!")
  232. return
  233. }
  234. let sens = sensitivities.sensitivities.map { item -> NightscoutTimevalue in
  235. NightscoutTimevalue(
  236. time: String(item.start.prefix(5)),
  237. value: item.sensitivity,
  238. timeAsSeconds: item.offset
  239. )
  240. }
  241. let target_low = targets.targets.map { item -> NightscoutTimevalue in
  242. NightscoutTimevalue(
  243. time: String(item.start.prefix(5)),
  244. value: item.low,
  245. timeAsSeconds: item.offset
  246. )
  247. }
  248. let target_high = targets.targets.map { item -> NightscoutTimevalue in
  249. NightscoutTimevalue(
  250. time: String(item.start.prefix(5)),
  251. value: item.high,
  252. timeAsSeconds: item.offset
  253. )
  254. }
  255. let cr = carbRatios.schedule.map { item -> NightscoutTimevalue in
  256. NightscoutTimevalue(
  257. time: String(item.start.prefix(5)),
  258. value: item.ratio,
  259. timeAsSeconds: item.offset
  260. )
  261. }
  262. let basal = basalProfile.map { item -> NightscoutTimevalue in
  263. NightscoutTimevalue(
  264. time: String(item.start.prefix(5)),
  265. value: item.rate,
  266. timeAsSeconds: item.minutes * 60
  267. )
  268. }
  269. var nsUnits = ""
  270. switch settingsManager.settings.units {
  271. case .mgdL:
  272. nsUnits = "mg/dl"
  273. case .mmolL:
  274. nsUnits = "mmol"
  275. }
  276. var carbs_hr: Decimal = 0
  277. if let isf = sensitivities.sensitivities.map(\.sensitivity).first,
  278. let cr = carbRatios.schedule.map(\.ratio).first,
  279. isf > 0, cr > 0
  280. {
  281. // CarbImpact -> Carbs/hr = CI [mg/dl/5min] * 12 / ISF [mg/dl/U] * CR [g/U]
  282. carbs_hr = settingsManager.preferences.min5mCarbimpact * 12 / isf * cr
  283. if settingsManager.settings.units == .mmolL {
  284. carbs_hr = carbs_hr * GlucoseUnits.exchangeRate
  285. }
  286. // No, Decimal has no rounding function.
  287. carbs_hr = Decimal(round(Double(carbs_hr) * 10.0)) / 10
  288. }
  289. let ps = ScheduledNightscoutProfile(
  290. dia: settingsManager.pumpSettings.insulinActionCurve,
  291. carbs_hr: Int(carbs_hr),
  292. delay: 0,
  293. timezone: TimeZone.current.identifier,
  294. target_low: target_low,
  295. target_high: target_high,
  296. sens: sens,
  297. basal: basal,
  298. carbratio: cr,
  299. units: nsUnits
  300. )
  301. let defaultProfile = "default"
  302. let now = Date()
  303. let p = NightscoutProfileStore(
  304. defaultProfile: defaultProfile,
  305. startDate: now,
  306. mills: Int(now.timeIntervalSince1970) * 1000,
  307. units: nsUnits,
  308. enteredBy: NigtscoutTreatment.local,
  309. store: [defaultProfile: ps]
  310. )
  311. if let uploadedProfile = storage.retrieve(OpenAPS.Nightscout.uploadedProfile, as: NightscoutProfileStore.self),
  312. (uploadedProfile.store[defaultProfile]?.rawJSON ?? "") == ps.rawJSON
  313. {
  314. NSLog("NightscoutManager uploadProfile, no profile change")
  315. return
  316. }
  317. guard let nightscout = nightscoutAPI, isNetworkReachable, isUploadEnabled else {
  318. return
  319. }
  320. processQueue.async {
  321. nightscout.uploadProfile(p)
  322. .sink { completion in
  323. switch completion {
  324. case .finished:
  325. self.storage.save(p, as: OpenAPS.Nightscout.uploadedProfile)
  326. debug(.nightscout, "Profile uploaded")
  327. case let .failure(error):
  328. debug(.nightscout, error.localizedDescription)
  329. }
  330. } receiveValue: {}
  331. .store(in: &self.lifetime)
  332. }
  333. }
  334. func uploadGlucose() {
  335. uploadGlucose(glucoseStorage.nightscoutGlucoseNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedGlucose)
  336. }
  337. private func uploadPumpHistory() {
  338. uploadTreatments(pumpHistoryStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedPumphistory)
  339. }
  340. private func uploadCarbs() {
  341. uploadTreatments(carbsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCarbs)
  342. }
  343. private func uploadTempTargets() {
  344. uploadTreatments(tempTargetsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedTempTargets)
  345. }
  346. private func uploadGlucose(_ glucose: [BloodGlucose], fileToSave: String) {
  347. guard !glucose.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled, isUploadGlucoseEnabled else {
  348. return
  349. }
  350. processQueue.async {
  351. glucose.chunks(ofCount: 100)
  352. .map { chunk -> AnyPublisher<Void, Error> in
  353. nightscout.uploadGlucose(Array(chunk))
  354. }
  355. .reduce(
  356. Just(()).setFailureType(to: Error.self)
  357. .eraseToAnyPublisher()
  358. ) { (result, next) -> AnyPublisher<Void, Error> in
  359. Publishers.Concatenate(prefix: result, suffix: next).eraseToAnyPublisher()
  360. }
  361. .dropFirst()
  362. .sink { completion in
  363. switch completion {
  364. case .finished:
  365. self.storage.save(glucose, as: fileToSave)
  366. case let .failure(error):
  367. debug(.nightscout, error.localizedDescription)
  368. }
  369. } receiveValue: {}
  370. .store(in: &self.lifetime)
  371. }
  372. }
  373. private func uploadTreatments(_ treatments: [NigtscoutTreatment], fileToSave: String) {
  374. guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
  375. return
  376. }
  377. processQueue.async {
  378. treatments.chunks(ofCount: 100)
  379. .map { chunk -> AnyPublisher<Void, Error> in
  380. nightscout.uploadTreatments(Array(chunk))
  381. }
  382. .reduce(
  383. Just(()).setFailureType(to: Error.self)
  384. .eraseToAnyPublisher()
  385. ) { (result, next) -> AnyPublisher<Void, Error> in
  386. Publishers.Concatenate(prefix: result, suffix: next).eraseToAnyPublisher()
  387. }
  388. .dropFirst()
  389. .sink { completion in
  390. switch completion {
  391. case .finished:
  392. self.storage.save(treatments, as: fileToSave)
  393. case let .failure(error):
  394. debug(.nightscout, error.localizedDescription)
  395. }
  396. } receiveValue: {}
  397. .store(in: &self.lifetime)
  398. }
  399. }
  400. }
  401. extension BaseNightscoutManager: PumpHistoryObserver {
  402. func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
  403. uploadPumpHistory()
  404. }
  405. }
  406. extension BaseNightscoutManager: CarbsObserver {
  407. func carbsDidUpdate(_: [CarbsEntry]) {
  408. uploadCarbs()
  409. }
  410. }
  411. extension BaseNightscoutManager: TempTargetsObserver {
  412. func tempTargetsDidUpdate(_: [TempTarget]) {
  413. uploadTempTargets()
  414. }
  415. }