NightscoutAPI.swift 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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 = "feeaps-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 fetchTempTargets(sinceDate: Date? = nil) -> AnyPublisher<[TempTarget], 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[eventType]", value: "Temporary+Target"),
  121. URLQueryItem(
  122. name: "find[enteredBy][$ne]",
  123. value: TempTarget.manual.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  124. ),
  125. URLQueryItem(
  126. name: "find[enteredBy][$ne]",
  127. value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  128. )
  129. ]
  130. if let date = sinceDate {
  131. let dateItem = URLQueryItem(
  132. name: "find[created_at][$gte]",
  133. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  134. )
  135. components.queryItems?.append(dateItem)
  136. }
  137. var request = URLRequest(url: components.url!)
  138. request.allowsConstrainedNetworkAccess = false
  139. request.timeoutInterval = Config.timeout
  140. if let secret = secret {
  141. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  142. }
  143. return service.run(request)
  144. .retry(Config.retryCount)
  145. .decode(type: [TempTarget].self, decoder: JSONCoding.decoder)
  146. .eraseToAnyPublisher()
  147. }
  148. func fetchAnnouncement(sinceDate: Date? = nil) -> AnyPublisher<[Announcement], Swift.Error> {
  149. var components = URLComponents()
  150. components.scheme = url.scheme
  151. components.host = url.host
  152. components.port = url.port
  153. components.path = Config.treatmentsPath
  154. components.queryItems = [
  155. URLQueryItem(name: "find[eventType]", value: "Announcement"),
  156. URLQueryItem(
  157. name: "find[enteredBy]",
  158. value: Announcement.remote.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  159. )
  160. ]
  161. if let date = sinceDate {
  162. let dateItem = URLQueryItem(
  163. name: "find[created_at][$gte]",
  164. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  165. )
  166. components.queryItems?.append(dateItem)
  167. }
  168. var request = URLRequest(url: components.url!)
  169. request.allowsConstrainedNetworkAccess = false
  170. request.timeoutInterval = Config.timeout
  171. if let secret = secret {
  172. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  173. }
  174. return service.run(request)
  175. .retry(Config.retryCount)
  176. .decode(type: [Announcement].self, decoder: JSONCoding.decoder)
  177. .eraseToAnyPublisher()
  178. }
  179. func uploadTreatments(_ treatments: [NigtscoutTreatment]) -> AnyPublisher<Void, Swift.Error> {
  180. var components = URLComponents()
  181. components.scheme = url.scheme
  182. components.host = url.host
  183. components.port = url.port
  184. components.path = Config.treatmentsPath
  185. var request = URLRequest(url: components.url!)
  186. request.allowsConstrainedNetworkAccess = false
  187. request.timeoutInterval = Config.timeout
  188. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  189. if let secret = secret {
  190. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  191. }
  192. request.httpBody = try! JSONCoding.encoder.encode(treatments)
  193. request.httpMethod = "POST"
  194. return service.run(request)
  195. .retry(Config.retryCount)
  196. .map { _ in () }
  197. .eraseToAnyPublisher()
  198. }
  199. func uploadStatus(_ status: NightscoutStatus) -> AnyPublisher<Void, Swift.Error> {
  200. var components = URLComponents()
  201. components.scheme = url.scheme
  202. components.host = url.host
  203. components.port = url.port
  204. components.path = Config.statusPath
  205. var request = URLRequest(url: components.url!)
  206. request.allowsConstrainedNetworkAccess = false
  207. request.timeoutInterval = Config.timeout
  208. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  209. if let secret = secret {
  210. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  211. }
  212. request.httpBody = try! JSONCoding.encoder.encode(status)
  213. request.httpMethod = "POST"
  214. return service.run(request)
  215. .retry(Config.retryCount)
  216. .map { _ in () }
  217. .eraseToAnyPublisher()
  218. }
  219. }
  220. private extension String {
  221. func sha1() -> String {
  222. let data = Data(utf8)
  223. var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
  224. data.withUnsafeBytes {
  225. _ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest)
  226. }
  227. let hexBytes = digest.map { String(format: "%02hhx", $0) }
  228. return hexBytes.joined()
  229. }
  230. }