NightscoutAPI.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  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 = "iAPS"
  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 deleteManualGlucose(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[glucose][$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 deleteInsulin(at date: Date) -> AnyPublisher<Void, 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[bolus][$exists]", value: "true"),
  184. URLQueryItem(
  185. name: "find[created_at][$eq]",
  186. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  187. )
  188. ]
  189. var request = URLRequest(url: components.url!)
  190. request.allowsConstrainedNetworkAccess = false
  191. request.timeoutInterval = Config.timeout
  192. request.httpMethod = "DELETE"
  193. if let secret = secret {
  194. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  195. }
  196. return service.run(request)
  197. .retry(Config.retryCount)
  198. .map { _ in () }
  199. .eraseToAnyPublisher()
  200. }
  201. func fetchTempTargets(sinceDate: Date? = nil) -> AnyPublisher<[TempTarget], Swift.Error> {
  202. var components = URLComponents()
  203. components.scheme = url.scheme
  204. components.host = url.host
  205. components.port = url.port
  206. components.path = Config.treatmentsPath
  207. components.queryItems = [
  208. URLQueryItem(name: "find[eventType]", value: "Temporary+Target"),
  209. URLQueryItem(
  210. name: "find[enteredBy][$ne]",
  211. value: TempTarget.manual.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  212. ),
  213. URLQueryItem(
  214. name: "find[enteredBy][$ne]",
  215. value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  216. ),
  217. URLQueryItem(name: "find[duration][$exists]", value: "true")
  218. ]
  219. if let date = sinceDate {
  220. let dateItem = URLQueryItem(
  221. name: "find[created_at][$gt]",
  222. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  223. )
  224. components.queryItems?.append(dateItem)
  225. }
  226. var request = URLRequest(url: components.url!)
  227. request.allowsConstrainedNetworkAccess = false
  228. request.timeoutInterval = Config.timeout
  229. if let secret = secret {
  230. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  231. }
  232. return service.run(request)
  233. .retry(Config.retryCount)
  234. .decode(type: [TempTarget].self, decoder: JSONCoding.decoder)
  235. .catch { error -> AnyPublisher<[TempTarget], Swift.Error> in
  236. warning(.nightscout, "TempTarget fetching error: \(error.localizedDescription)")
  237. return Just([]).setFailureType(to: Swift.Error.self).eraseToAnyPublisher()
  238. }
  239. .eraseToAnyPublisher()
  240. }
  241. func fetchAnnouncement(sinceDate: Date? = nil) -> AnyPublisher<[Announcement], Swift.Error> {
  242. var components = URLComponents()
  243. components.scheme = url.scheme
  244. components.host = url.host
  245. components.port = url.port
  246. components.path = Config.treatmentsPath
  247. components.queryItems = [
  248. URLQueryItem(name: "find[eventType]", value: "Announcement"),
  249. URLQueryItem(
  250. name: "find[enteredBy]",
  251. value: Announcement.remote.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  252. )
  253. ]
  254. if let date = sinceDate {
  255. let dateItem = URLQueryItem(
  256. name: "find[created_at][$gte]",
  257. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  258. )
  259. components.queryItems?.append(dateItem)
  260. }
  261. var request = URLRequest(url: components.url!)
  262. request.allowsConstrainedNetworkAccess = false
  263. request.timeoutInterval = Config.timeout
  264. if let secret = secret {
  265. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  266. }
  267. return service.run(request)
  268. .retry(Config.retryCount)
  269. .decode(type: [Announcement].self, decoder: JSONCoding.decoder)
  270. .eraseToAnyPublisher()
  271. }
  272. func uploadTreatments(_ treatments: [NigtscoutTreatment]) -> AnyPublisher<Void, Swift.Error> {
  273. var components = URLComponents()
  274. components.scheme = url.scheme
  275. components.host = url.host
  276. components.port = url.port
  277. components.path = Config.treatmentsPath
  278. var request = URLRequest(url: components.url!)
  279. request.allowsConstrainedNetworkAccess = false
  280. request.timeoutInterval = Config.timeout
  281. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  282. if let secret = secret {
  283. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  284. }
  285. request.httpBody = try! JSONCoding.encoder.encode(treatments)
  286. request.httpMethod = "POST"
  287. return service.run(request)
  288. .retry(Config.retryCount)
  289. .map { _ in () }
  290. .eraseToAnyPublisher()
  291. }
  292. func uploadGlucose(_ glucose: [BloodGlucose]) -> AnyPublisher<Void, Swift.Error> {
  293. var components = URLComponents()
  294. components.scheme = url.scheme
  295. components.host = url.host
  296. components.port = url.port
  297. components.path = Config.uploadEntriesPath
  298. var request = URLRequest(url: components.url!)
  299. request.allowsConstrainedNetworkAccess = false
  300. request.timeoutInterval = Config.timeout
  301. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  302. if let secret = secret {
  303. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  304. }
  305. request.httpBody = try! JSONCoding.encoder.encode(glucose)
  306. request.httpMethod = "POST"
  307. return service.run(request)
  308. .retry(Config.retryCount)
  309. .map { _ in () }
  310. .eraseToAnyPublisher()
  311. }
  312. func uploadStats(_ stats: NightscoutStatistics) -> AnyPublisher<Void, Swift.Error> {
  313. var components = URLComponents()
  314. components.scheme = url.scheme
  315. components.host = url.host
  316. components.port = url.port
  317. components.path = Config.statusPath
  318. var request = URLRequest(url: components.url!)
  319. request.allowsConstrainedNetworkAccess = false
  320. request.timeoutInterval = Config.timeout
  321. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  322. if let secret = secret {
  323. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  324. }
  325. request.httpBody = try! JSONCoding.encoder.encode(stats)
  326. request.httpMethod = "POST"
  327. return service.run(request)
  328. .retry(Config.retryCount)
  329. .map { _ in () }
  330. .eraseToAnyPublisher()
  331. }
  332. func uploadStatus(_ status: NightscoutStatus) -> AnyPublisher<Void, Swift.Error> {
  333. var components = URLComponents()
  334. components.scheme = url.scheme
  335. components.host = url.host
  336. components.port = url.port
  337. components.path = Config.statusPath
  338. var request = URLRequest(url: components.url!)
  339. request.allowsConstrainedNetworkAccess = false
  340. request.timeoutInterval = Config.timeout
  341. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  342. if let secret = secret {
  343. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  344. }
  345. request.httpBody = try! JSONCoding.encoder.encode(status)
  346. request.httpMethod = "POST"
  347. return service.run(request)
  348. .retry(Config.retryCount)
  349. .map { _ in () }
  350. .eraseToAnyPublisher()
  351. }
  352. func uploadPrefs(_ prefs: NightscoutPreferences) -> AnyPublisher<Void, Swift.Error> {
  353. var components = URLComponents()
  354. components.scheme = url.scheme
  355. components.host = url.host
  356. components.port = url.port
  357. components.path = Config.statusPath
  358. var request = URLRequest(url: components.url!)
  359. request.allowsConstrainedNetworkAccess = false
  360. request.timeoutInterval = Config.timeout
  361. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  362. if let secret = secret {
  363. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  364. }
  365. request.httpBody = try! JSONCoding.encoder.encode(prefs)
  366. request.httpMethod = "POST"
  367. return service.run(request)
  368. .retry(Config.retryCount)
  369. .map { _ in () }
  370. .eraseToAnyPublisher()
  371. }
  372. func uploadSettings(_ settings: NightscoutSettings) -> AnyPublisher<Void, Swift.Error> {
  373. var components = URLComponents()
  374. components.scheme = url.scheme
  375. components.host = url.host
  376. components.port = url.port
  377. components.path = Config.statusPath
  378. var request = URLRequest(url: components.url!)
  379. request.allowsConstrainedNetworkAccess = false
  380. request.timeoutInterval = Config.timeout
  381. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  382. if let secret = secret {
  383. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  384. }
  385. request.httpBody = try! JSONCoding.encoder.encode(settings)
  386. request.httpMethod = "POST"
  387. return service.run(request)
  388. .retry(Config.retryCount)
  389. .map { _ in () }
  390. .eraseToAnyPublisher()
  391. }
  392. func uploadProfile(_ profile: NightscoutProfileStore) -> AnyPublisher<Void, Swift.Error> {
  393. var components = URLComponents()
  394. components.scheme = url.scheme
  395. components.host = url.host
  396. components.port = url.port
  397. components.path = Config.profilePath
  398. var request = URLRequest(url: components.url!)
  399. request.allowsConstrainedNetworkAccess = false
  400. request.timeoutInterval = Config.timeout
  401. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  402. if let secret = secret {
  403. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  404. }
  405. request.httpBody = try! JSONCoding.encoder.encode(profile)
  406. request.httpMethod = "POST"
  407. return service.run(request)
  408. .retry(Config.retryCount)
  409. .map { _ in () }
  410. .eraseToAnyPublisher()
  411. }
  412. func uploadPreferences(_ preferences: Preferences) -> AnyPublisher<Void, Swift.Error> {
  413. var components = URLComponents()
  414. components.scheme = url.scheme
  415. components.host = url.host
  416. components.port = url.port
  417. components.path = Config.profilePath
  418. var request = URLRequest(url: components.url!)
  419. request.allowsConstrainedNetworkAccess = false
  420. request.timeoutInterval = Config.timeout
  421. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  422. if let secret = secret {
  423. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  424. }
  425. request.httpBody = try! JSONCoding.encoder.encode(preferences)
  426. request.httpMethod = "POST"
  427. return service.run(request)
  428. .retry(Config.retryCount)
  429. .map { _ in () }
  430. .eraseToAnyPublisher()
  431. }
  432. }
  433. private extension String {
  434. func sha1() -> String {
  435. let data = Data(utf8)
  436. var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
  437. data.withUnsafeBytes {
  438. _ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest)
  439. }
  440. let hexBytes = digest.map { String(format: "%02hhx", $0) }
  441. return hexBytes.joined()
  442. }
  443. }