NightscoutAPI.swift 11 KB

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