NightscoutManager.swift 9.6 KB

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