NightscoutManager.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  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: NigtscoutTreatment.local,
  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. }
  199. func uploadProfile() {
  200. // These should be modified anyways and not the defaults
  201. guard let sensitivities = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self),
  202. let basalProfile = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self),
  203. let carbRatios = storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self),
  204. let targets = storage.retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self)
  205. else {
  206. NSLog("NightscoutManager uploadProfile Not all settings found to build profile!")
  207. return
  208. }
  209. let sens = sensitivities.sensitivities.map { item -> NightscoutTimevalue in
  210. NightscoutTimevalue(
  211. time: String(item.start.prefix(5)),
  212. value: item.sensitivity,
  213. timeAsSeconds: item.offset
  214. )
  215. }
  216. let target_low = targets.targets.map { item -> NightscoutTimevalue in
  217. NightscoutTimevalue(
  218. time: String(item.start.prefix(5)),
  219. value: item.low,
  220. timeAsSeconds: item.offset
  221. )
  222. }
  223. let target_high = targets.targets.map { item -> NightscoutTimevalue in
  224. NightscoutTimevalue(
  225. time: String(item.start.prefix(5)),
  226. value: item.high,
  227. timeAsSeconds: item.offset
  228. )
  229. }
  230. let cr = carbRatios.schedule.map { item -> NightscoutTimevalue in
  231. NightscoutTimevalue(
  232. time: String(item.start.prefix(5)),
  233. value: item.ratio,
  234. timeAsSeconds: item.offset
  235. )
  236. }
  237. let basal = basalProfile.map { item -> NightscoutTimevalue in
  238. NightscoutTimevalue(
  239. time: String(item.start.prefix(5)),
  240. value: item.rate,
  241. timeAsSeconds: item.minutes * 60
  242. )
  243. }
  244. let ps = ScheduledNightscoutProfile(
  245. dia: settingsManager.pumpSettings.insulinActionCurve,
  246. carbs_hr: settingsManager.preferences.min5mCarbimpact * 12,
  247. delay: 0,
  248. timezone: TimeZone.current.identifier,
  249. target_low: target_low,
  250. target_high: target_high,
  251. sens: sens,
  252. basal: basal,
  253. carbratio: cr
  254. )
  255. let defaultProfile = "default"
  256. let now = Date()
  257. let p = NightscoutProfileStore(
  258. defaultProfile: defaultProfile,
  259. startDate: now,
  260. mills: Int(now.timeIntervalSince1970),
  261. units: String(describing: settingsManager.settings.units),
  262. enteredBy: NigtscoutTreatment.local,
  263. store: [defaultProfile: ps]
  264. )
  265. NSLog(p.rawJSON)
  266. if let uploadedProfile = storage.retrieve(OpenAPS.Nightscout.uploadedProfile, as: NightscoutProfileStore.self),
  267. (uploadedProfile.store[defaultProfile]?.rawJSON ?? "") == ps.rawJSON
  268. {
  269. NSLog("NightscoutManager uploadProfile, no profile change")
  270. return
  271. }
  272. guard let nightscout = nightscoutAPI, isNetworkReachable, isUploadEnabled else {
  273. return // Just([]).eraseToAnyPublisher()
  274. }
  275. processQueue.async {
  276. nightscout.uploadProfile(p)
  277. .sink { completion in
  278. switch completion {
  279. case .finished:
  280. self.storage.save(p, as: OpenAPS.Nightscout.uploadedProfile)
  281. debug(.nightscout, "Profile uploaded")
  282. case let .failure(error):
  283. debug(.nightscout, error.localizedDescription)
  284. }
  285. } receiveValue: {}
  286. .store(in: &self.lifetime)
  287. }
  288. }
  289. func uploadGlucose() {
  290. uploadGlucose(glucoseStorage.nightscoutGlucoseNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedGlucose)
  291. }
  292. private func uploadPumpHistory() {
  293. uploadTreatments(pumpHistoryStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedPumphistory)
  294. }
  295. private func uploadCarbs() {
  296. uploadTreatments(carbsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCarbs)
  297. }
  298. private func uploadTempTargets() {
  299. uploadTreatments(tempTargetsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedTempTargets)
  300. }
  301. private func uploadGlucose(_ glucose: [BloodGlucose], fileToSave: String) {
  302. guard !glucose.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled, isUploadGlucoseEnabled else {
  303. return
  304. }
  305. processQueue.async {
  306. glucose.chunks(ofCount: 100)
  307. .map { chunk -> AnyPublisher<Void, Error> in
  308. nightscout.uploadGlucose(Array(chunk))
  309. }
  310. .reduce(
  311. Just(()).setFailureType(to: Error.self)
  312. .eraseToAnyPublisher()
  313. ) { (result, next) -> AnyPublisher<Void, Error> in
  314. Publishers.Concatenate(prefix: result, suffix: next).eraseToAnyPublisher()
  315. }
  316. .dropFirst()
  317. .sink { completion in
  318. switch completion {
  319. case .finished:
  320. self.storage.save(glucose, as: fileToSave)
  321. case let .failure(error):
  322. debug(.nightscout, error.localizedDescription)
  323. }
  324. } receiveValue: {}
  325. .store(in: &self.lifetime)
  326. }
  327. }
  328. private func uploadTreatments(_ treatments: [NigtscoutTreatment], fileToSave: String) {
  329. guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
  330. return
  331. }
  332. processQueue.async {
  333. treatments.chunks(ofCount: 100)
  334. .map { chunk -> AnyPublisher<Void, Error> in
  335. nightscout.uploadTreatments(Array(chunk))
  336. }
  337. .reduce(
  338. Just(()).setFailureType(to: Error.self)
  339. .eraseToAnyPublisher()
  340. ) { (result, next) -> AnyPublisher<Void, Error> in
  341. Publishers.Concatenate(prefix: result, suffix: next).eraseToAnyPublisher()
  342. }
  343. .dropFirst()
  344. .sink { completion in
  345. switch completion {
  346. case .finished:
  347. self.storage.save(treatments, as: fileToSave)
  348. case let .failure(error):
  349. debug(.nightscout, error.localizedDescription)
  350. }
  351. } receiveValue: {}
  352. .store(in: &self.lifetime)
  353. }
  354. }
  355. }
  356. extension BaseNightscoutManager: PumpHistoryObserver {
  357. func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
  358. uploadPumpHistory()
  359. }
  360. }
  361. extension BaseNightscoutManager: CarbsObserver {
  362. func carbsDidUpdate(_: [CarbsEntry]) {
  363. uploadCarbs()
  364. }
  365. }
  366. extension BaseNightscoutManager: TempTargetsObserver {
  367. func tempTargetsDidUpdate(_: [TempTarget]) {
  368. uploadTempTargets()
  369. }
  370. }