NightscoutAPI.swift 19 KB

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