NightscoutAPI.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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. .map { glucose in
  69. glucose
  70. .map {
  71. var reading = $0
  72. reading.glucose = $0.sgv
  73. return reading
  74. }
  75. }
  76. .eraseToAnyPublisher()
  77. }
  78. func fetchCarbs(sinceDate: Date? = nil) -> AnyPublisher<[CarbsEntry], Swift.Error> {
  79. var components = URLComponents()
  80. components.scheme = url.scheme
  81. components.host = url.host
  82. components.port = url.port
  83. components.path = Config.treatmentsPath
  84. components.queryItems = [
  85. URLQueryItem(name: "find[carbs][$exists]", value: "true"),
  86. URLQueryItem(
  87. name: "find[enteredBy][$ne]",
  88. value: CarbsEntry.manual.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  89. ),
  90. URLQueryItem(
  91. name: "find[enteredBy][$ne]",
  92. value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  93. )
  94. ]
  95. if let date = sinceDate {
  96. let dateItem = URLQueryItem(
  97. name: "find[created_at][$gte]",
  98. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  99. )
  100. components.queryItems?.append(dateItem)
  101. }
  102. var request = URLRequest(url: components.url!)
  103. request.allowsConstrainedNetworkAccess = false
  104. request.timeoutInterval = Config.timeout
  105. if let secret = secret {
  106. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  107. }
  108. return service.run(request)
  109. .retry(Config.retryCount)
  110. .decode(type: [CarbsEntry].self, decoder: JSONCoding.decoder)
  111. .eraseToAnyPublisher()
  112. }
  113. func deleteCarbs(at date: Date) -> AnyPublisher<Void, Swift.Error> {
  114. var components = URLComponents()
  115. components.scheme = url.scheme
  116. components.host = url.host
  117. components.port = url.port
  118. components.path = Config.treatmentsPath
  119. components.queryItems = [
  120. URLQueryItem(name: "find[carbs][$exists]", value: "true"),
  121. URLQueryItem(
  122. name: "find[created_at][$eq]",
  123. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  124. )
  125. ]
  126. var request = URLRequest(url: components.url!)
  127. request.allowsConstrainedNetworkAccess = false
  128. request.timeoutInterval = Config.timeout
  129. request.httpMethod = "DELETE"
  130. if let secret = secret {
  131. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  132. }
  133. return service.run(request)
  134. .retry(Config.retryCount)
  135. .map { _ in () }
  136. .eraseToAnyPublisher()
  137. }
  138. func fetchTempTargets(sinceDate: Date? = nil) -> AnyPublisher<[TempTarget], Swift.Error> {
  139. var components = URLComponents()
  140. components.scheme = url.scheme
  141. components.host = url.host
  142. components.port = url.port
  143. components.path = Config.treatmentsPath
  144. components.queryItems = [
  145. URLQueryItem(name: "find[eventType]", value: "Temporary+Target"),
  146. URLQueryItem(
  147. name: "find[enteredBy][$ne]",
  148. value: TempTarget.manual.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  149. ),
  150. URLQueryItem(
  151. name: "find[enteredBy][$ne]",
  152. value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  153. )
  154. ]
  155. if let date = sinceDate {
  156. let dateItem = URLQueryItem(
  157. name: "find[created_at][$gte]",
  158. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  159. )
  160. components.queryItems?.append(dateItem)
  161. }
  162. var request = URLRequest(url: components.url!)
  163. request.allowsConstrainedNetworkAccess = false
  164. request.timeoutInterval = Config.timeout
  165. if let secret = secret {
  166. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  167. }
  168. return service.run(request)
  169. .retry(Config.retryCount)
  170. .decode(type: [TempTarget].self, decoder: JSONCoding.decoder)
  171. .eraseToAnyPublisher()
  172. }
  173. func fetchAnnouncement(sinceDate: Date? = nil) -> AnyPublisher<[Announcement], Swift.Error> {
  174. var components = URLComponents()
  175. components.scheme = url.scheme
  176. components.host = url.host
  177. components.port = url.port
  178. components.path = Config.treatmentsPath
  179. components.queryItems = [
  180. URLQueryItem(name: "find[eventType]", value: "Announcement"),
  181. URLQueryItem(
  182. name: "find[enteredBy]",
  183. value: Announcement.remote.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  184. )
  185. ]
  186. if let date = sinceDate {
  187. let dateItem = URLQueryItem(
  188. name: "find[created_at][$gte]",
  189. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  190. )
  191. components.queryItems?.append(dateItem)
  192. }
  193. var request = URLRequest(url: components.url!)
  194. request.allowsConstrainedNetworkAccess = false
  195. request.timeoutInterval = Config.timeout
  196. if let secret = secret {
  197. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  198. }
  199. return service.run(request)
  200. .retry(Config.retryCount)
  201. .decode(type: [Announcement].self, decoder: JSONCoding.decoder)
  202. .eraseToAnyPublisher()
  203. }
  204. func uploadTreatments(_ treatments: [NigtscoutTreatment]) -> AnyPublisher<Void, Swift.Error> {
  205. var components = URLComponents()
  206. components.scheme = url.scheme
  207. components.host = url.host
  208. components.port = url.port
  209. components.path = Config.treatmentsPath
  210. var request = URLRequest(url: components.url!)
  211. request.allowsConstrainedNetworkAccess = false
  212. request.timeoutInterval = Config.timeout
  213. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  214. if let secret = secret {
  215. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  216. }
  217. request.httpBody = try! JSONCoding.encoder.encode(treatments)
  218. request.httpMethod = "POST"
  219. return service.run(request)
  220. .retry(Config.retryCount)
  221. .map { _ in () }
  222. .eraseToAnyPublisher()
  223. }
  224. func uploadStatus(_ status: NightscoutStatus) -> AnyPublisher<Void, Swift.Error> {
  225. var components = URLComponents()
  226. components.scheme = url.scheme
  227. components.host = url.host
  228. components.port = url.port
  229. components.path = Config.statusPath
  230. var request = URLRequest(url: components.url!)
  231. request.allowsConstrainedNetworkAccess = false
  232. request.timeoutInterval = Config.timeout
  233. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  234. if let secret = secret {
  235. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  236. }
  237. request.httpBody = try! JSONCoding.encoder.encode(status)
  238. request.httpMethod = "POST"
  239. return service.run(request)
  240. .retry(Config.retryCount)
  241. .map { _ in () }
  242. .eraseToAnyPublisher()
  243. }
  244. }
  245. private extension String {
  246. func sha1() -> String {
  247. let data = Data(utf8)
  248. var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
  249. data.withUnsafeBytes {
  250. _ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest)
  251. }
  252. let hexBytes = digest.map { String(format: "%02hhx", $0) }
  253. return hexBytes.joined()
  254. }
  255. }