NightscoutAPI.swift 19 KB

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