|
|
@@ -30,14 +30,15 @@ public enum ShareError: Error {
|
|
|
public enum KnownShareServers: String {
|
|
|
case US="https://share2.dexcom.com"
|
|
|
case NON_US="https://shareous1.dexcom.com"
|
|
|
-
|
|
|
+
|
|
|
}
|
|
|
|
|
|
// From the Dexcom Share iOS app, via @bewest and @shanselman:
|
|
|
// https://github.com/bewest/share2nightscout-bridge
|
|
|
private let dexcomUserAgent = "Dexcom Share/3.0.2.11 CFNetwork/711.2.23 Darwin/14.0.0"
|
|
|
private let dexcomApplicationId = "d89443d2-327c-4a6f-89e5-496bbb0317db"
|
|
|
-private let dexcomLoginPath = "/ShareWebServices/Services/General/LoginPublisherAccountByName"
|
|
|
+private let dexcomAuthenticatePath = "/ShareWebServices/Services/General/AuthenticatePublisherAccount"
|
|
|
+private let dexcomLoginByIdPath = "/ShareWebServices/Services/General/LoginPublisherAccountById"
|
|
|
private let dexcomLatestGlucosePath = "/ShareWebServices/Services/Publisher/ReadPublisherLatestGlucoseValues"
|
|
|
private let maxReauthAttempts = 2
|
|
|
|
|
|
@@ -46,22 +47,22 @@ private let maxReauthAttempts = 2
|
|
|
// ¯\_(ツ)_/¯
|
|
|
private func dexcomPOST(_ url: URL, JSONData: [String: AnyObject]? = nil, callback: @escaping (Error?, String?) -> Void) {
|
|
|
var data: Data?
|
|
|
-
|
|
|
+
|
|
|
if let JSONData = JSONData {
|
|
|
guard let encoded = try? JSONSerialization.data(withJSONObject: JSONData, options:[]) else {
|
|
|
return callback(ShareError.dataError(reason: "Failed to encode JSON for POST to " + url.absoluteString), nil)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
data = encoded
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
var request = URLRequest(url: url)
|
|
|
request.httpMethod = "POST"
|
|
|
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
|
request.addValue("application/json", forHTTPHeaderField: "Accept")
|
|
|
request.addValue(dexcomUserAgent, forHTTPHeaderField: "User-Agent")
|
|
|
request.httpBody = data
|
|
|
-
|
|
|
+
|
|
|
URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
|
|
|
if error != nil {
|
|
|
callback(error, nil)
|
|
|
@@ -74,63 +75,104 @@ private func dexcomPOST(_ url: URL, JSONData: [String: AnyObject]? = nil, callba
|
|
|
public class ShareClient {
|
|
|
public let username: String
|
|
|
public let password: String
|
|
|
-
|
|
|
+
|
|
|
private let shareServer:String
|
|
|
private var token: String?
|
|
|
-
|
|
|
+
|
|
|
public init(username: String, password: String, shareServer:String=KnownShareServers.US.rawValue) {
|
|
|
self.username = username
|
|
|
self.password = password
|
|
|
self.shareServer = shareServer
|
|
|
}
|
|
|
public convenience init(username: String, password: String, shareServer:KnownShareServers=KnownShareServers.US) {
|
|
|
-
|
|
|
+
|
|
|
self.init(username: username, password: password, shareServer:shareServer.rawValue)
|
|
|
-
|
|
|
+
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
public func fetchLast(_ n: Int, callback: @escaping (ShareError?, [ShareGlucose]?) -> Void) {
|
|
|
fetchLastWithRetries(n, remaining: maxReauthAttempts, callback: callback)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
private func ensureToken(_ callback: @escaping (ShareError?) -> Void) {
|
|
|
if token != nil {
|
|
|
callback(nil)
|
|
|
} else {
|
|
|
- fetchToken() { (error, token) in
|
|
|
- if error != nil {
|
|
|
+ fetchAccountID { result in
|
|
|
+ switch result {
|
|
|
+ case .failure(let error):
|
|
|
callback(error)
|
|
|
- } else {
|
|
|
- self.token = token
|
|
|
- callback(nil)
|
|
|
+ case .success(let accountId):
|
|
|
+ self.fetchTokenByAccountId(accountId) { (error, token) in
|
|
|
+ if error != nil {
|
|
|
+ callback(error)
|
|
|
+ } else {
|
|
|
+ self.token = token
|
|
|
+ callback(nil)
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- private func fetchToken(_ callback: @escaping (ShareError?, String?) -> Void) {
|
|
|
+
|
|
|
+ private func fetchAccountID(_ callback: @escaping (Result<String, ShareError>) -> Void) {
|
|
|
let data = [
|
|
|
"accountName": username,
|
|
|
"password": password,
|
|
|
"applicationId": dexcomApplicationId
|
|
|
]
|
|
|
-
|
|
|
- guard let url = URL(string: shareServer + dexcomLoginPath) else {
|
|
|
+
|
|
|
+ guard let url = URL(string: shareServer + dexcomAuthenticatePath) else {
|
|
|
+ return callback(.failure(.fetchError))
|
|
|
+ }
|
|
|
+
|
|
|
+ dexcomPOST(url, JSONData: data as [String : AnyObject]?) { (error, response) in
|
|
|
+ if let error = error {
|
|
|
+ return callback(.failure(.httpError(error)))
|
|
|
+ }
|
|
|
+
|
|
|
+ guard let response = response,
|
|
|
+ let data = response.data(using: .utf8),
|
|
|
+ let decoded = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
|
|
|
+ else {
|
|
|
+ return callback(.failure(.loginError(errorCode: "unknown")))
|
|
|
+ }
|
|
|
+
|
|
|
+ if let token = decoded as? String {
|
|
|
+ // success is a JSON-encoded string containing the token
|
|
|
+ callback(.success(token))
|
|
|
+ } else {
|
|
|
+ // failure is a JSON object containing the error reason
|
|
|
+ let errorCode = (decoded as? [String: String])?["Code"] ?? "unknown"
|
|
|
+ callback(.failure(.loginError(errorCode: errorCode)))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func fetchTokenByAccountId(_ accountId: String, callback: @escaping (ShareError?, String?) -> Void) {
|
|
|
+ let data = [
|
|
|
+ "accountId": accountId,
|
|
|
+ "password": password,
|
|
|
+ "applicationId": dexcomApplicationId
|
|
|
+ ]
|
|
|
+
|
|
|
+ guard let url = URL(string: shareServer + dexcomLoginByIdPath) else {
|
|
|
return callback(ShareError.fetchError, nil)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
dexcomPOST(url, JSONData: data as [String : AnyObject]?) { (error, response) in
|
|
|
if let error = error {
|
|
|
return callback(.httpError(error), nil)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
guard let response = response,
|
|
|
- let data = response.data(using: .utf8),
|
|
|
- let decoded = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
|
|
|
- else {
|
|
|
- return callback(.loginError(errorCode: "unknown"), nil)
|
|
|
+ let data = response.data(using: .utf8),
|
|
|
+ let decoded = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
|
|
|
+ else {
|
|
|
+ return callback(.loginError(errorCode: "unknown"), nil)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
if let token = decoded as? String {
|
|
|
// success is a JSON-encoded string containing the token
|
|
|
callback(nil, token)
|
|
|
@@ -141,37 +183,37 @@ public class ShareClient {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
private func fetchLastWithRetries(_ n: Int, remaining: Int, callback: @escaping (ShareError?, [ShareGlucose]?) -> Void) {
|
|
|
ensureToken() { (error) in
|
|
|
guard error == nil else {
|
|
|
return callback(error, nil)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
guard var components = URLComponents(string: self.shareServer + dexcomLatestGlucosePath) else {
|
|
|
return callback(.fetchError, nil)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
components.queryItems = [
|
|
|
URLQueryItem(name: "sessionId", value: self.token),
|
|
|
URLQueryItem(name: "minutes", value: String(1440)),
|
|
|
URLQueryItem(name: "maxCount", value: String(n))
|
|
|
]
|
|
|
-
|
|
|
+
|
|
|
guard let url = components.url else {
|
|
|
return callback(.fetchError, nil)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
dexcomPOST(url) { (error, response) in
|
|
|
if let error = error {
|
|
|
return callback(.httpError(error), nil)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
do {
|
|
|
guard let response = response else {
|
|
|
throw ShareError.fetchError
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
let decoded = try? JSONSerialization.jsonObject(with: response.data(using: .utf8)!, options: [])
|
|
|
guard let sgvs = decoded as? Array<AnyObject> else {
|
|
|
if remaining > 0 {
|
|
|
@@ -181,24 +223,28 @@ public class ShareClient {
|
|
|
throw ShareError.dataError(reason: "Failed to decode SGVs as array after trying to reauth: " + response)
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
var transformed: Array<ShareGlucose> = []
|
|
|
- // Dec 2021, Dexcom Share modified json encoding of Trend from int to string
|
|
|
- let trendmap = ["": 0, "DoubleUp":1, "SingleUp":2, "FortyFiveUp":3, "Flat":4, "FortyFiveDown":5, "SingleDown":6, "DoubleDown": 7, "NotComputable":8, "RateOutOfRange":9]
|
|
|
for sgv in sgvs {
|
|
|
- if let glucose = sgv["Value"] as? Int, let strend = sgv["Trend"] as? String, let wt = sgv["WT"] as? String {
|
|
|
- let itrend = trendmap[strend, default: 0]
|
|
|
- transformed.append(ShareGlucose(
|
|
|
- glucose: UInt16(glucose),
|
|
|
- trend: UInt8(itrend),
|
|
|
- timestamp: try self.parseDate(wt)
|
|
|
- ))
|
|
|
- } else if let glucose = sgv["Value"] as? Int, let trend = sgv["Trend"] as? Int, let wt = sgv["WT"] as? String {
|
|
|
- transformed.append(ShareGlucose(
|
|
|
- glucose: UInt16(glucose),
|
|
|
- trend: UInt8(trend),
|
|
|
- timestamp: try self.parseDate(wt)
|
|
|
- ))
|
|
|
+ if let glucose = sgv["Value"] as? Int, let wt = sgv["WT"] as? String {
|
|
|
+ let trend: Int?
|
|
|
+ if let trendString = sgv["Trend"] as? String {
|
|
|
+ // Dec 2021, Dexcom Share modified json encoding of Trend from int to string
|
|
|
+ let trendmap = ["": 0, "DoubleUp":1, "SingleUp":2, "FortyFiveUp":3, "Flat":4, "FortyFiveDown":5, "SingleDown":6, "DoubleDown": 7, "NotComputable":8, "RateOutOfRange":9]
|
|
|
+ trend = trendmap[trendString, default: 0]
|
|
|
+ } else {
|
|
|
+ trend = sgv["Trend"] as? Int
|
|
|
+ }
|
|
|
+
|
|
|
+ if let trend = trend {
|
|
|
+ transformed.append(ShareGlucose(
|
|
|
+ glucose: UInt16(glucose),
|
|
|
+ trend: UInt8(trend),
|
|
|
+ timestamp: try self.parseDate(wt)
|
|
|
+ ))
|
|
|
+ } else {
|
|
|
+ throw ShareError.dataError(reason: "Failed to decode. SGV record had bad trend: " + response)
|
|
|
+ }
|
|
|
} else {
|
|
|
throw ShareError.dataError(reason: "Failed to decode an SGV record: " + response)
|
|
|
}
|
|
|
@@ -212,16 +258,16 @@ public class ShareClient {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
private func parseDate(_ wt: String) throws -> Date {
|
|
|
// wt looks like "/Date(1462404576000)/"
|
|
|
let re = try NSRegularExpression(pattern: "\\((.*)\\)")
|
|
|
if let match = re.firstMatch(in: wt, range: NSMakeRange(0, wt.count)) {
|
|
|
- #if swift(>=4)
|
|
|
- let matchRange = match.range(at: 1)
|
|
|
- #else
|
|
|
- let matchRange = match.rangeAt(1)
|
|
|
- #endif
|
|
|
+#if swift(>=4)
|
|
|
+ let matchRange = match.range(at: 1)
|
|
|
+#else
|
|
|
+ let matchRange = match.rangeAt(1)
|
|
|
+#endif
|
|
|
let epoch = Double((wt as NSString).substring(with: matchRange))! / 1000
|
|
|
return Date(timeIntervalSince1970: epoch)
|
|
|
} else {
|