NightscoutAPI.swift 8.3 KB

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