NightscoutAPI.swift 15 KB

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