NightscoutAPI.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. import Combine
  2. import CommonCrypto
  3. import Foundation
  4. import JavaScriptCore
  5. import Swinject
  6. class NightscoutAPI {
  7. init(url: URL, secret: String? = nil) {
  8. self.url = url
  9. self.secret = secret?.nonEmpty
  10. }
  11. private enum Config {
  12. static let entriesPath = "/api/v1/entries/sgv.json"
  13. static let uploadEntriesPath = "/api/v1/entries.json"
  14. static let treatmentsPath = "/api/v1/treatments.json"
  15. static let statusPath = "/api/v1/devicestatus.json"
  16. static let profilePath = "/api/v1/profile.json"
  17. static let retryCount = 1
  18. static let timeout: TimeInterval = 60
  19. }
  20. enum Error: LocalizedError {
  21. case badStatusCode
  22. case missingURL
  23. }
  24. let url: URL
  25. let secret: String?
  26. private let service = NetworkService()
  27. @Injected() private var settingsManager: SettingsManager!
  28. }
  29. extension NightscoutAPI {
  30. func checkConnection() -> AnyPublisher<Void, Swift.Error> {
  31. struct Check: Codable, Equatable {
  32. var eventType = "Note"
  33. var enteredBy = "freeaps-x"
  34. var notes = "iAPS connected"
  35. }
  36. let check = Check()
  37. var request = URLRequest(url: url.appendingPathComponent(Config.treatmentsPath))
  38. if let secret = secret {
  39. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  40. request.httpMethod = "POST"
  41. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  42. request.httpBody = try! JSONCoding.encoder.encode(check)
  43. } else {
  44. request.httpMethod = "GET"
  45. }
  46. return service.run(request)
  47. .map { _ in () }
  48. .eraseToAnyPublisher()
  49. }
  50. func fetchLastGlucose(sinceDate: Date? = nil) -> AnyPublisher<[BloodGlucose], Swift.Error> {
  51. var components = URLComponents()
  52. components.scheme = url.scheme
  53. components.host = url.host
  54. components.port = url.port
  55. components.path = Config.entriesPath
  56. components.queryItems = [URLQueryItem(name: "count", value: "\(1600)")]
  57. if let date = sinceDate {
  58. let dateItem = URLQueryItem(
  59. name: "find[dateString][$gte]",
  60. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  61. )
  62. components.queryItems?.append(dateItem)
  63. }
  64. var request = URLRequest(url: components.url!)
  65. request.allowsConstrainedNetworkAccess = false
  66. request.timeoutInterval = Config.timeout
  67. if let secret = secret {
  68. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  69. }
  70. return service.run(request)
  71. .retry(Config.retryCount)
  72. .decode(type: [BloodGlucose].self, decoder: JSONCoding.decoder)
  73. .catch { error -> AnyPublisher<[BloodGlucose], Swift.Error> in
  74. warning(.nightscout, "Glucose fetching error: \(error.localizedDescription)")
  75. return Just([]).setFailureType(to: Swift.Error.self).eraseToAnyPublisher()
  76. }
  77. .map { glucose in
  78. glucose
  79. .map {
  80. var reading = $0
  81. reading.glucose = $0.sgv
  82. return reading
  83. }
  84. }
  85. .eraseToAnyPublisher()
  86. }
  87. func fetchCarbs(sinceDate: Date? = nil) -> AnyPublisher<[CarbsEntry], Swift.Error> {
  88. var components = URLComponents()
  89. components.scheme = url.scheme
  90. components.host = url.host
  91. components.port = url.port
  92. components.path = Config.treatmentsPath
  93. components.queryItems = [
  94. URLQueryItem(name: "find[carbs][$exists]", value: "true"),
  95. URLQueryItem(
  96. name: "find[enteredBy][$ne]",
  97. value: CarbsEntry.manual.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  98. ),
  99. URLQueryItem(
  100. name: "find[enteredBy][$ne]",
  101. value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  102. )
  103. ]
  104. if let date = sinceDate {
  105. let dateItem = URLQueryItem(
  106. name: "find[created_at][$gt]",
  107. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  108. )
  109. components.queryItems?.append(dateItem)
  110. }
  111. var request = URLRequest(url: components.url!)
  112. request.allowsConstrainedNetworkAccess = false
  113. request.timeoutInterval = Config.timeout
  114. if let secret = secret {
  115. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  116. }
  117. return service.run(request)
  118. .retry(Config.retryCount)
  119. .decode(type: [CarbsEntry].self, decoder: JSONCoding.decoder)
  120. .catch { error -> AnyPublisher<[CarbsEntry], Swift.Error> in
  121. warning(.nightscout, "Carbs fetching error: \(error.localizedDescription)")
  122. return Just([]).setFailureType(to: Swift.Error.self).eraseToAnyPublisher()
  123. }
  124. .eraseToAnyPublisher()
  125. }
  126. func deleteCarbs(at date: Date) -> AnyPublisher<Void, Swift.Error> {
  127. var components = URLComponents()
  128. components.scheme = url.scheme
  129. components.host = url.host
  130. components.port = url.port
  131. components.path = Config.treatmentsPath
  132. components.queryItems = [
  133. URLQueryItem(name: "find[carbs][$exists]", value: "true"),
  134. URLQueryItem(
  135. name: "find[created_at][$eq]",
  136. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  137. )
  138. ]
  139. var request = URLRequest(url: components.url!)
  140. request.allowsConstrainedNetworkAccess = false
  141. request.timeoutInterval = Config.timeout
  142. request.httpMethod = "DELETE"
  143. if let secret = secret {
  144. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  145. }
  146. return service.run(request)
  147. .retry(Config.retryCount)
  148. .map { _ in () }
  149. .eraseToAnyPublisher()
  150. }
  151. func deleteInsulin(at date: Date) -> AnyPublisher<Void, Swift.Error> {
  152. var components = URLComponents()
  153. components.scheme = url.scheme
  154. components.host = url.host
  155. components.port = url.port
  156. components.path = Config.treatmentsPath
  157. components.queryItems = [
  158. URLQueryItem(name: "find[bolus][$exists]", value: "true"),
  159. URLQueryItem(
  160. name: "find[created_at][$eq]",
  161. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  162. )
  163. ]
  164. var request = URLRequest(url: components.url!)
  165. request.allowsConstrainedNetworkAccess = false
  166. request.timeoutInterval = Config.timeout
  167. request.httpMethod = "DELETE"
  168. if let secret = secret {
  169. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  170. }
  171. return service.run(request)
  172. .retry(Config.retryCount)
  173. .map { _ in () }
  174. .eraseToAnyPublisher()
  175. }
  176. func fetchTempTargets(sinceDate: Date? = nil) -> AnyPublisher<[TempTarget], Swift.Error> {
  177. var components = URLComponents()
  178. components.scheme = url.scheme
  179. components.host = url.host
  180. components.port = url.port
  181. components.path = Config.treatmentsPath
  182. components.queryItems = [
  183. URLQueryItem(name: "find[eventType]", value: "Temporary+Target"),
  184. URLQueryItem(
  185. name: "find[enteredBy][$ne]",
  186. value: TempTarget.manual.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  187. ),
  188. URLQueryItem(
  189. name: "find[enteredBy][$ne]",
  190. value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  191. ),
  192. URLQueryItem(name: "find[duration][$exists]", value: "true")
  193. ]
  194. if let date = sinceDate {
  195. let dateItem = URLQueryItem(
  196. name: "find[created_at][$gt]",
  197. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  198. )
  199. components.queryItems?.append(dateItem)
  200. }
  201. var request = URLRequest(url: components.url!)
  202. request.allowsConstrainedNetworkAccess = false
  203. request.timeoutInterval = Config.timeout
  204. if let secret = secret {
  205. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  206. }
  207. return service.run(request)
  208. .retry(Config.retryCount)
  209. .decode(type: [TempTarget].self, decoder: JSONCoding.decoder)
  210. .catch { error -> AnyPublisher<[TempTarget], Swift.Error> in
  211. warning(.nightscout, "TempTarget fetching error: \(error.localizedDescription)")
  212. return Just([]).setFailureType(to: Swift.Error.self).eraseToAnyPublisher()
  213. }
  214. .eraseToAnyPublisher()
  215. }
  216. func fetchAnnouncement(sinceDate: Date? = nil) -> AnyPublisher<[Announcement], Swift.Error> {
  217. var components = URLComponents()
  218. components.scheme = url.scheme
  219. components.host = url.host
  220. components.port = url.port
  221. components.path = Config.treatmentsPath
  222. components.queryItems = [
  223. URLQueryItem(name: "find[eventType]", value: "Announcement"),
  224. URLQueryItem(
  225. name: "find[enteredBy]",
  226. value: Announcement.remote.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  227. )
  228. ]
  229. if let date = sinceDate {
  230. let dateItem = URLQueryItem(
  231. name: "find[created_at][$gte]",
  232. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  233. )
  234. components.queryItems?.append(dateItem)
  235. }
  236. var request = URLRequest(url: components.url!)
  237. request.allowsConstrainedNetworkAccess = false
  238. request.timeoutInterval = Config.timeout
  239. if let secret = secret {
  240. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  241. }
  242. return service.run(request)
  243. .retry(Config.retryCount)
  244. .decode(type: [Announcement].self, decoder: JSONCoding.decoder)
  245. .eraseToAnyPublisher()
  246. }
  247. func uploadTreatments(_ treatments: [NigtscoutTreatment]) -> AnyPublisher<Void, Swift.Error> {
  248. var components = URLComponents()
  249. components.scheme = url.scheme
  250. components.host = url.host
  251. components.port = url.port
  252. components.path = Config.treatmentsPath
  253. var request = URLRequest(url: components.url!)
  254. request.allowsConstrainedNetworkAccess = false
  255. request.timeoutInterval = Config.timeout
  256. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  257. if let secret = secret {
  258. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  259. }
  260. request.httpBody = try! JSONCoding.encoder.encode(treatments)
  261. request.httpMethod = "POST"
  262. return service.run(request)
  263. .retry(Config.retryCount)
  264. .map { _ in () }
  265. .eraseToAnyPublisher()
  266. }
  267. func uploadGlucose(_ glucose: [BloodGlucose]) -> AnyPublisher<Void, Swift.Error> {
  268. var components = URLComponents()
  269. components.scheme = url.scheme
  270. components.host = url.host
  271. components.port = url.port
  272. components.path = Config.uploadEntriesPath
  273. var request = URLRequest(url: components.url!)
  274. request.allowsConstrainedNetworkAccess = false
  275. request.timeoutInterval = Config.timeout
  276. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  277. if let secret = secret {
  278. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  279. }
  280. request.httpBody = try! JSONCoding.encoder.encode(glucose)
  281. request.httpMethod = "POST"
  282. return service.run(request)
  283. .retry(Config.retryCount)
  284. .map { _ in () }
  285. .eraseToAnyPublisher()
  286. }
  287. func uploadStats(_ stats: NightscoutStatistics) -> AnyPublisher<Void, Swift.Error> {
  288. var components = URLComponents()
  289. components.scheme = url.scheme
  290. components.host = url.host
  291. components.port = url.port
  292. components.path = Config.statusPath
  293. var request = URLRequest(url: components.url!)
  294. request.allowsConstrainedNetworkAccess = false
  295. request.timeoutInterval = Config.timeout
  296. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  297. if let secret = secret {
  298. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  299. }
  300. request.httpBody = try! JSONCoding.encoder.encode(stats)
  301. request.httpMethod = "POST"
  302. return service.run(request)
  303. .retry(Config.retryCount)
  304. .map { _ in () }
  305. .eraseToAnyPublisher()
  306. }
  307. func uploadStatus(_ status: NightscoutStatus) -> AnyPublisher<Void, Swift.Error> {
  308. var components = URLComponents()
  309. components.scheme = url.scheme
  310. components.host = url.host
  311. components.port = url.port
  312. components.path = Config.statusPath
  313. var request = URLRequest(url: components.url!)
  314. request.allowsConstrainedNetworkAccess = false
  315. request.timeoutInterval = Config.timeout
  316. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  317. if let secret = secret {
  318. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  319. }
  320. request.httpBody = try! JSONCoding.encoder.encode(status)
  321. request.httpMethod = "POST"
  322. return service.run(request)
  323. .retry(Config.retryCount)
  324. .map { _ in () }
  325. .eraseToAnyPublisher()
  326. }
  327. func uploadPrefs(_ prefs: NightscoutPreferences) -> AnyPublisher<Void, Swift.Error> {
  328. var components = URLComponents()
  329. components.scheme = url.scheme
  330. components.host = url.host
  331. components.port = url.port
  332. components.path = Config.statusPath
  333. var request = URLRequest(url: components.url!)
  334. request.allowsConstrainedNetworkAccess = false
  335. request.timeoutInterval = Config.timeout
  336. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  337. if let secret = secret {
  338. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  339. }
  340. request.httpBody = try! JSONCoding.encoder.encode(prefs)
  341. request.httpMethod = "POST"
  342. return service.run(request)
  343. .retry(Config.retryCount)
  344. .map { _ in () }
  345. .eraseToAnyPublisher()
  346. }
  347. func uploadSettings(_ settings: NightscoutSettings) -> AnyPublisher<Void, Swift.Error> {
  348. var components = URLComponents()
  349. components.scheme = url.scheme
  350. components.host = url.host
  351. components.port = url.port
  352. components.path = Config.statusPath
  353. var request = URLRequest(url: components.url!)
  354. request.allowsConstrainedNetworkAccess = false
  355. request.timeoutInterval = Config.timeout
  356. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  357. if let secret = secret {
  358. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  359. }
  360. request.httpBody = try! JSONCoding.encoder.encode(settings)
  361. request.httpMethod = "POST"
  362. return service.run(request)
  363. .retry(Config.retryCount)
  364. .map { _ in () }
  365. .eraseToAnyPublisher()
  366. }
  367. func uploadProfile(_ profile: NightscoutProfileStore) -> AnyPublisher<Void, Swift.Error> {
  368. var components = URLComponents()
  369. components.scheme = url.scheme
  370. components.host = url.host
  371. components.port = url.port
  372. components.path = Config.profilePath
  373. var request = URLRequest(url: components.url!)
  374. request.allowsConstrainedNetworkAccess = false
  375. request.timeoutInterval = Config.timeout
  376. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  377. if let secret = secret {
  378. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  379. }
  380. request.httpBody = try! JSONCoding.encoder.encode(profile)
  381. request.httpMethod = "POST"
  382. return service.run(request)
  383. .retry(Config.retryCount)
  384. .map { _ in () }
  385. .eraseToAnyPublisher()
  386. }
  387. }
  388. private extension String {
  389. func sha1() -> String {
  390. let data = Data(utf8)
  391. var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
  392. data.withUnsafeBytes {
  393. _ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest)
  394. }
  395. let hexBytes = digest.map { String(format: "%02hhx", $0) }
  396. return hexBytes.joined()
  397. }
  398. }