NightscoutManager.swift 8.9 KB

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