NightscoutManager.swift 11 KB

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