NightscoutAPI.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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][$gt]",
  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. URLQueryItem(name: "find[duration][$exists]", value: "true")
  155. ]
  156. if let date = sinceDate {
  157. let dateItem = URLQueryItem(
  158. name: "find[created_at][$gt]",
  159. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  160. )
  161. components.queryItems?.append(dateItem)
  162. }
  163. var request = URLRequest(url: components.url!)
  164. request.allowsConstrainedNetworkAccess = false
  165. request.timeoutInterval = Config.timeout
  166. if let secret = secret {
  167. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  168. }
  169. return service.run(request)
  170. .retry(Config.retryCount)
  171. .decode(type: [TempTarget].self, decoder: JSONCoding.decoder)
  172. .eraseToAnyPublisher()
  173. }
  174. func fetchAnnouncement(sinceDate: Date? = nil) -> AnyPublisher<[Announcement], Swift.Error> {
  175. var components = URLComponents()
  176. components.scheme = url.scheme
  177. components.host = url.host
  178. components.port = url.port
  179. components.path = Config.treatmentsPath
  180. components.queryItems = [
  181. URLQueryItem(name: "find[eventType]", value: "Announcement"),
  182. URLQueryItem(
  183. name: "find[enteredBy]",
  184. value: Announcement.remote.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
  185. )
  186. ]
  187. if let date = sinceDate {
  188. let dateItem = URLQueryItem(
  189. name: "find[created_at][$gte]",
  190. value: Formatter.iso8601withFractionalSeconds.string(from: date)
  191. )
  192. components.queryItems?.append(dateItem)
  193. }
  194. var request = URLRequest(url: components.url!)
  195. request.allowsConstrainedNetworkAccess = false
  196. request.timeoutInterval = Config.timeout
  197. if let secret = secret {
  198. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  199. }
  200. return service.run(request)
  201. .retry(Config.retryCount)
  202. .decode(type: [Announcement].self, decoder: JSONCoding.decoder)
  203. .eraseToAnyPublisher()
  204. }
  205. func uploadTreatments(_ treatments: [NigtscoutTreatment]) -> AnyPublisher<Void, Swift.Error> {
  206. var components = URLComponents()
  207. components.scheme = url.scheme
  208. components.host = url.host
  209. components.port = url.port
  210. components.path = Config.treatmentsPath
  211. var request = URLRequest(url: components.url!)
  212. request.allowsConstrainedNetworkAccess = false
  213. request.timeoutInterval = Config.timeout
  214. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  215. if let secret = secret {
  216. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  217. }
  218. request.httpBody = try! JSONCoding.encoder.encode(treatments)
  219. request.httpMethod = "POST"
  220. return service.run(request)
  221. .retry(Config.retryCount)
  222. .map { _ in () }
  223. .eraseToAnyPublisher()
  224. }
  225. func uploadStatus(_ status: NightscoutStatus) -> AnyPublisher<Void, Swift.Error> {
  226. var components = URLComponents()
  227. components.scheme = url.scheme
  228. components.host = url.host
  229. components.port = url.port
  230. components.path = Config.statusPath
  231. var request = URLRequest(url: components.url!)
  232. request.allowsConstrainedNetworkAccess = false
  233. request.timeoutInterval = Config.timeout
  234. request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  235. if let secret = secret {
  236. request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  237. }
  238. request.httpBody = try! JSONCoding.encoder.encode(status)
  239. request.httpMethod = "POST"
  240. return service.run(request)
  241. .retry(Config.retryCount)
  242. .map { _ in () }
  243. .eraseToAnyPublisher()
  244. }
  245. }
  246. private extension String {
  247. func sha1() -> String {
  248. let data = Data(utf8)
  249. var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
  250. data.withUnsafeBytes {
  251. _ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest)
  252. }
  253. let hexBytes = digest.map { String(format: "%02hhx", $0) }
  254. return hexBytes.joined()
  255. }
  256. }