NightscoutAPI.swift 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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 = 30
  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 = "feeaps-x://"
  29. var notes = "FreeAPS X connected"
  30. }
  31. let check = Check()
  32. var request = URLRequest(url: url.appendingPathComponent(Config.treatmentsPath))
  33. request.httpMethod = "POST"
  34. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  35. if let secret = secret {
  36. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  37. }
  38. request.httpBody = try! JSONCoding.encoder.encode(check)
  39. return service.run(request)
  40. .map { _ in () }
  41. .eraseToAnyPublisher()
  42. }
  43. func fetchLastGlucose(_ count: Int, sinceDate: Date? = nil) -> AnyPublisher<[BloodGlucose], Swift.Error> {
  44. var components = URLComponents()
  45. components.scheme = url.scheme
  46. components.host = url.host
  47. components.port = url.port
  48. components.path = Config.entriesPath
  49. components.queryItems = [URLQueryItem(name: "count", value: "\(count)")]
  50. if let date = sinceDate {
  51. let dateItem = URLQueryItem(
  52. name: "find[dateString][$gte]",
  53. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  54. )
  55. components.queryItems?.append(dateItem)
  56. }
  57. var request = URLRequest(url: components.url!)
  58. request.allowsConstrainedNetworkAccess = false
  59. request.timeoutInterval = Config.timeout
  60. if let secret = secret {
  61. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  62. }
  63. return service.run(request)
  64. .retry(Config.retryCount)
  65. .decode(type: [BloodGlucose].self, decoder: JSONCoding.decoder)
  66. .map {
  67. $0.filter { $0.isStateValid }
  68. .map {
  69. var reading = $0
  70. reading.glucose = $0.sgv
  71. return reading
  72. }
  73. }
  74. .eraseToAnyPublisher()
  75. }
  76. func fetchCarbs(sinceDate: Date? = nil) -> AnyPublisher<[CarbsEntry], Swift.Error> {
  77. var components = URLComponents()
  78. components.scheme = url.scheme
  79. components.host = url.host
  80. components.port = url.port
  81. components.path = Config.treatmentsPath
  82. components.queryItems = [
  83. URLQueryItem(name: "find[carbs][$exists]", value: "true"),
  84. URLQueryItem(
  85. name: "find[enteredBy][$ne]",
  86. value: CarbsEntry.manual.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  87. ),
  88. URLQueryItem(
  89. name: "find[enteredBy][$ne]",
  90. value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  91. )
  92. ]
  93. if let date = sinceDate {
  94. let dateItem = URLQueryItem(
  95. name: "find[created_at][$gte]",
  96. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  97. )
  98. components.queryItems?.append(dateItem)
  99. }
  100. var request = URLRequest(url: components.url!)
  101. request.allowsConstrainedNetworkAccess = false
  102. request.timeoutInterval = Config.timeout
  103. if let secret = secret {
  104. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  105. }
  106. return service.run(request)
  107. .retry(Config.retryCount)
  108. .decode(type: [CarbsEntry].self, decoder: JSONCoding.decoder)
  109. .eraseToAnyPublisher()
  110. }
  111. func fetchTempTargets(sinceDate: Date? = nil) -> AnyPublisher<[TempTarget], Swift.Error> {
  112. var components = URLComponents()
  113. components.scheme = url.scheme
  114. components.host = url.host
  115. components.port = url.port
  116. components.path = Config.treatmentsPath
  117. components.queryItems = [
  118. URLQueryItem(name: "find[eventType]", value: "Temporary+Target"),
  119. URLQueryItem(
  120. name: "find[enteredBy][$ne]",
  121. value: TempTarget.manual.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  122. ),
  123. URLQueryItem(
  124. name: "find[enteredBy][$ne]",
  125. value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  126. )
  127. ]
  128. if let date = sinceDate {
  129. let dateItem = URLQueryItem(
  130. name: "find[created_at][$gte]",
  131. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  132. )
  133. components.queryItems?.append(dateItem)
  134. }
  135. var request = URLRequest(url: components.url!)
  136. request.allowsConstrainedNetworkAccess = false
  137. request.timeoutInterval = Config.timeout
  138. if let secret = secret {
  139. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  140. }
  141. return service.run(request)
  142. .retry(Config.retryCount)
  143. .decode(type: [TempTarget].self, decoder: JSONCoding.decoder)
  144. .eraseToAnyPublisher()
  145. }
  146. func fetchAnnouncement(sinceDate: Date? = nil) -> AnyPublisher<[Announcement], 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: "Announcement"),
  154. URLQueryItem(
  155. name: "find[enteredBy]",
  156. value: Announcement.remote.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  157. )
  158. ]
  159. if let date = sinceDate {
  160. let dateItem = URLQueryItem(
  161. name: "find[created_at][$gte]",
  162. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  163. )
  164. components.queryItems?.append(dateItem)
  165. }
  166. var request = URLRequest(url: components.url!)
  167. request.allowsConstrainedNetworkAccess = false
  168. request.timeoutInterval = Config.timeout
  169. if let secret = secret {
  170. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  171. }
  172. return service.run(request)
  173. .retry(Config.retryCount)
  174. .decode(type: [Announcement].self, decoder: JSONCoding.decoder)
  175. .eraseToAnyPublisher()
  176. }
  177. func uploadTreatments(_ treatments: [NigtscoutTreatment]) -> 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. var request = URLRequest(url: components.url!)
  184. request.allowsConstrainedNetworkAccess = false
  185. request.timeoutInterval = Config.timeout
  186. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  187. if let secret = secret {
  188. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  189. }
  190. request.httpBody = try! JSONCoding.encoder.encode(treatments)
  191. request.httpMethod = "POST"
  192. return service.run(request)
  193. .retry(Config.retryCount)
  194. .map { _ in () }
  195. .eraseToAnyPublisher()
  196. }
  197. func uploadStatus(_ status: NightscoutStatus) -> AnyPublisher<Void, Swift.Error> {
  198. var components = URLComponents()
  199. components.scheme = url.scheme
  200. components.host = url.host
  201. components.port = url.port
  202. components.path = Config.statusPath
  203. var request = URLRequest(url: components.url!)
  204. request.allowsConstrainedNetworkAccess = false
  205. request.timeoutInterval = Config.timeout
  206. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  207. if let secret = secret {
  208. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  209. }
  210. request.httpBody = try! JSONCoding.encoder.encode(status)
  211. request.httpMethod = "POST"
  212. return service.run(request)
  213. .retry(Config.retryCount)
  214. .map { _ in () }
  215. .eraseToAnyPublisher()
  216. }
  217. }
  218. private extension String {
  219. func sha1() -> String {
  220. let data = Data(utf8)
  221. var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
  222. data.withUnsafeBytes {
  223. _ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest)
  224. }
  225. let hexBytes = digest.map { String(format: "%02hhx", $0) }
  226. return hexBytes.joined()
  227. }
  228. }