NightscoutManager.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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. var cgmURL: URL? { get }
  14. }
  15. final class BaseNightscoutManager: NightscoutManager, Injectable {
  16. @Injected() private var keychain: Keychain!
  17. @Injected() private var glucoseStorage: GlucoseStorage!
  18. @Injected() private var tempTargetsStorage: TempTargetsStorage!
  19. @Injected() private var carbsStorage: CarbsStorage!
  20. @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
  21. @Injected() private var storage: FileStorage!
  22. @Injected() private var announcementsStorage: AnnouncementsStorage!
  23. @Injected() private var settingsManager: SettingsManager!
  24. @Injected() private var broadcaster: Broadcaster!
  25. @Injected() private var reachabilityManager: ReachabilityManager!
  26. private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
  27. private var ping: TimeInterval?
  28. private var lifetime = Lifetime()
  29. private var isNetworkReachable: Bool {
  30. reachabilityManager.isReachable
  31. }
  32. private var isUploadEnabled: Bool {
  33. settingsManager.settings.isUploadEnabled
  34. }
  35. private var isUploadGlucoseEnabled: Bool {
  36. settingsManager.settings.uploadGlucose
  37. }
  38. private var nightscoutAPI: NightscoutAPI? {
  39. guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
  40. let url = URL(string: urlString),
  41. let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
  42. else {
  43. return nil
  44. }
  45. return NightscoutAPI(url: url, secret: secret)
  46. }
  47. init(resolver: Resolver) {
  48. injectServices(resolver)
  49. subscribe()
  50. }
  51. private func subscribe() {
  52. broadcaster.register(PumpHistoryObserver.self, observer: self)
  53. broadcaster.register(CarbsObserver.self, observer: self)
  54. broadcaster.register(TempTargetsObserver.self, observer: self)
  55. _ = reachabilityManager.startListening(onQueue: processQueue) { status in
  56. debug(.nightscout, "Network status: \(status)")
  57. }
  58. }
  59. func sourceInfo() -> [String: Any]? {
  60. if let ping = ping {
  61. return [GlucoseSourceKey.nightscoutPing.rawValue: ping]
  62. }
  63. return nil
  64. }
  65. var cgmURL: URL? {
  66. if let url = settingsManager.settings.cgm.appURL {
  67. return url
  68. }
  69. let useLocal = settingsManager.settings.useLocalGlucoseSource
  70. let maybeNightscout = useLocal
  71. ? NightscoutAPI(url: URL(string: "http://127.0.0.1:\(settingsManager.settings.localGlucosePort)")!)
  72. : nightscoutAPI
  73. return maybeNightscout?.url
  74. }
  75. func fetchGlucose(since date: Date) -> AnyPublisher<[BloodGlucose], Never> {
  76. let useLocal = settingsManager.settings.useLocalGlucoseSource
  77. ping = nil
  78. if !useLocal {
  79. guard isNetworkReachable else {
  80. return Just([]).eraseToAnyPublisher()
  81. }
  82. }
  83. let maybeNightscout = useLocal
  84. ? NightscoutAPI(url: URL(string: "http://127.0.0.1:\(settingsManager.settings.localGlucosePort)")!)
  85. : nightscoutAPI
  86. guard let nightscout = maybeNightscout else {
  87. return Just([]).eraseToAnyPublisher()
  88. }
  89. let startDate = Date()
  90. return nightscout.fetchLastGlucose(sinceDate: date)
  91. .tryCatch({ (error) -> AnyPublisher<[BloodGlucose], Error> in
  92. print(error.localizedDescription)
  93. return Just([]).setFailureType(to: Error.self).eraseToAnyPublisher()
  94. })
  95. .replaceError(with: [])
  96. .handleEvents(receiveOutput: { value in
  97. guard value.isNotEmpty else { return }
  98. self.ping = Date().timeIntervalSince(startDate)
  99. })
  100. .eraseToAnyPublisher()
  101. }
  102. func fetch() -> AnyPublisher<[BloodGlucose], Never> {
  103. fetchGlucose(since: glucoseStorage.syncDate())
  104. }
  105. func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never> {
  106. guard let nightscout = nightscoutAPI, isNetworkReachable else {
  107. return Just([]).eraseToAnyPublisher()
  108. }
  109. let since = carbsStorage.syncDate()
  110. return nightscout.fetchCarbs(sinceDate: since)
  111. .replaceError(with: [])
  112. .eraseToAnyPublisher()
  113. }
  114. func fetchTempTargets() -> AnyPublisher<[TempTarget], Never> {
  115. guard let nightscout = nightscoutAPI, isNetworkReachable else {
  116. return Just([]).eraseToAnyPublisher()
  117. }
  118. let since = tempTargetsStorage.syncDate()
  119. return nightscout.fetchTempTargets(sinceDate: since)
  120. .replaceError(with: [])
  121. .eraseToAnyPublisher()
  122. }
  123. func fetchAnnouncements() -> AnyPublisher<[Announcement], Never> {
  124. guard let nightscout = nightscoutAPI, isNetworkReachable else {
  125. return Just([]).eraseToAnyPublisher()
  126. }
  127. let since = announcementsStorage.syncDate()
  128. return nightscout.fetchAnnouncement(sinceDate: since)
  129. .replaceError(with: [])
  130. .eraseToAnyPublisher()
  131. }
  132. func deleteCarbs(at date: Date) {
  133. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  134. carbsStorage.deleteCarbs(at: date)
  135. return
  136. }
  137. nightscout.deleteCarbs(at: date)
  138. .sink { completion in
  139. switch completion {
  140. case .finished:
  141. self.carbsStorage.deleteCarbs(at: date)
  142. debug(.nightscout, "Carbs deleted")
  143. case let .failure(error):
  144. debug(.nightscout, error.localizedDescription)
  145. }
  146. } receiveValue: {}
  147. .store(in: &lifetime)
  148. }
  149. func uploadStatus() {
  150. let iob = storage.retrieve(OpenAPS.Monitor.iob, as: [IOBEntry].self)
  151. var suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self)
  152. var enacted = storage.retrieve(OpenAPS.Enact.enacted, as: Suggestion.self)
  153. if (suggested?.timestamp ?? .distantPast) > (enacted?.timestamp ?? .distantPast) {
  154. enacted?.predictions = nil
  155. } else {
  156. suggested?.predictions = nil
  157. }
  158. let openapsStatus = OpenAPSStatus(
  159. iob: iob?.first,
  160. suggested: suggested,
  161. enacted: enacted,
  162. version: "0.7.0"
  163. )
  164. let battery = storage.retrieve(OpenAPS.Monitor.battery, as: Battery.self)
  165. var reservoir = Decimal(from: storage.retrieveRaw(OpenAPS.Monitor.reservoir) ?? "0")
  166. if reservoir == 0xDEAD_BEEF {
  167. reservoir = nil
  168. }
  169. let pumpStatus = storage.retrieve(OpenAPS.Monitor.status, as: PumpStatus.self)
  170. let pump = NSPumpStatus(clock: Date(), battery: battery, reservoir: reservoir, status: pumpStatus)
  171. let preferences = settingsManager.preferences
  172. let device = UIDevice.current
  173. let uploader = Uploader(batteryVoltage: nil, battery: Int(device.batteryLevel * 100))
  174. let status = NightscoutStatus(
  175. device: "freeaps-x://" + device.name,
  176. openaps: openapsStatus,
  177. pump: pump,
  178. preferences: preferences,
  179. uploader: uploader
  180. )
  181. storage.save(status, as: OpenAPS.Upload.nsStatus)
  182. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  183. return
  184. }
  185. processQueue.async {
  186. nightscout.uploadStatus(status)
  187. .sink { completion in
  188. switch completion {
  189. case .finished:
  190. debug(.nightscout, "Status uploaded")
  191. case let .failure(error):
  192. debug(.nightscout, error.localizedDescription)
  193. }
  194. } receiveValue: {}
  195. .store(in: &self.lifetime)
  196. }
  197. }
  198. func uploadGlucose() {
  199. uploadGlucose(glucoseStorage.nightscoutGlucoseNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedGlucose)
  200. }
  201. private func uploadPumpHistory() {
  202. uploadTreatments(pumpHistoryStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedPumphistory)
  203. }
  204. private func uploadCarbs() {
  205. uploadTreatments(carbsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCarbs)
  206. }
  207. private func uploadTempTargets() {
  208. uploadTreatments(tempTargetsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedTempTargets)
  209. }
  210. private func uploadGlucose(_ glucose: [BloodGlucose], fileToSave: String) {
  211. guard !glucose.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled, isUploadGlucoseEnabled else {
  212. return
  213. }
  214. processQueue.async {
  215. glucose.chunks(ofCount: 100)
  216. .map { chunk -> AnyPublisher<Void, Error> in
  217. nightscout.uploadGlucose(Array(chunk))
  218. }
  219. .reduce(
  220. Just(()).setFailureType(to: Error.self)
  221. .eraseToAnyPublisher()
  222. ) { (result, next) -> AnyPublisher<Void, Error> in
  223. Publishers.Concatenate(prefix: result, suffix: next).eraseToAnyPublisher()
  224. }
  225. .dropFirst()
  226. .sink { completion in
  227. switch completion {
  228. case .finished:
  229. self.storage.save(glucose, as: fileToSave)
  230. case let .failure(error):
  231. debug(.nightscout, error.localizedDescription)
  232. }
  233. } receiveValue: {}
  234. .store(in: &self.lifetime)
  235. }
  236. }
  237. private func uploadTreatments(_ treatments: [NigtscoutTreatment], fileToSave: String) {
  238. guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
  239. return
  240. }
  241. processQueue.async {
  242. treatments.chunks(ofCount: 100)
  243. .map { chunk -> AnyPublisher<Void, Error> in
  244. nightscout.uploadTreatments(Array(chunk))
  245. }
  246. .reduce(
  247. Just(()).setFailureType(to: Error.self)
  248. .eraseToAnyPublisher()
  249. ) { (result, next) -> AnyPublisher<Void, Error> in
  250. Publishers.Concatenate(prefix: result, suffix: next).eraseToAnyPublisher()
  251. }
  252. .dropFirst()
  253. .sink { completion in
  254. switch completion {
  255. case .finished:
  256. self.storage.save(treatments, as: fileToSave)
  257. case let .failure(error):
  258. debug(.nightscout, error.localizedDescription)
  259. }
  260. } receiveValue: {}
  261. .store(in: &self.lifetime)
  262. }
  263. }
  264. }
  265. extension BaseNightscoutManager: PumpHistoryObserver {
  266. func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
  267. uploadPumpHistory()
  268. }
  269. }
  270. extension BaseNightscoutManager: CarbsObserver {
  271. func carbsDidUpdate(_: [CarbsEntry]) {
  272. uploadCarbs()
  273. }
  274. }
  275. extension BaseNightscoutManager: TempTargetsObserver {
  276. func tempTargetsDidUpdate(_: [TempTarget]) {
  277. uploadTempTargets()
  278. }
  279. }