Commit 0677389b by Demid Merzlyakov

Network: working with HealthCenter.

parent 6127b794
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
import Foundation import Foundation
public enum BlendHealthError: Error { public enum BlendHealthSourceError: Error {
case insufficientLocationInfo case insufficientLocationInfo
case badUrl case badUrl
case networkError(Error?) case networkError(Error?)
...@@ -17,6 +17,88 @@ public enum BlendHealthError: Error { ...@@ -17,6 +17,88 @@ public enum BlendHealthError: Error {
case alreadyBeingUpdated case alreadyBeingUpdated
} }
public class BlendHealthSource { public class BlendHealthSource: HealthSource {
private let log = Logger(componentName: "BlendHealthSource")
#warning("Not implemented: staging / prod switching!")
//TODO: Not implemented: staging / prod switching!
private static let healthCenterUrl = "http://sta-1w-dataaggregator.onelouder.com/1weather/api/v1/weather/current"
public var healthUpdateInterval: TimeInterval = TimeInterval(15 * 60) // 15 minutes
/// This queue is needed to synchronize access to locationsBeingUpdated. Also, to make logging more clear.
private let internalQueue: OperationQueue = {
let queue = OperationQueue()
queue.name = "BlendHealthSource Queue"
queue.maxConcurrentOperationCount = 1
return queue
}()
private var locationsBeingUpdated = Set<Location>()
public func updateHelath(for location: Location, completion: @escaping HealthSourceCompletion) {
internalQueue.addOperation { [weak self] in
let extendedCompletion: HealthSourceCompletion = { [weak self] (health, error) in
self?.internalQueue.addOperation {
completion(health, error)
self?.locationsBeingUpdated.remove(location)
}
}
self?.updateHelathInternal(for: location, completion: extendedCompletion)
}
}
private func updateHelathInternal(for location: Location, completion: @escaping HealthSourceCompletion) {
guard !locationsBeingUpdated.contains(location) else {
completion(nil, BlendHealthSourceError.alreadyBeingUpdated)
return
}
locationsBeingUpdated.insert(location)
guard var urlComponents = URLComponents(string: BlendHealthSource.healthCenterUrl) else {
assertionFailure("Should never happen. The URL should be correct.")
return
}
var queryParameters = [String: String]()
if let coordinates = location.coordinates {
queryParameters["LAT"] = String(format: "%.5f", coordinates.latitude)
queryParameters["LON"] = String(format: "%.5f", coordinates.longitude)
}
queryParameters["ZIP"] = location.zip
queryParameters["CITY"] = location.cityName
queryParameters["STATE"] = location.region
queryParameters["COUNTRY"] = location.countryName
guard !queryParameters.isEmpty else {
completion(nil, BlendHealthSourceError.insufficientLocationInfo)
log.error("Not enough information about location.")
return
}
guard let url = urlComponents.url else {
completion(nil, BlendHealthSourceError.badUrl)
return
}
log.debug("query params: \(queryParameters)")
let urlSession = URLSession.shared
let dataTask = urlSession.dataTask(with: url) { [weak self] (data, reponse, error) in
guard let self = self else { return }
guard let data = data else {
completion(nil, BlendHealthSourceError.networkError(error))
return
}
let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
do {
let updatedHealth = try decoder.decode(BlendHealthCenter.self, from: data)
let appHealth = updatedHealth.toAppModel()
completion(appHealth, nil)
}
catch {
completion(nil, BlendHealthSourceError.badServerResponse(error))
}
}
dataTask.resume()
}
} }
...@@ -12,9 +12,9 @@ import UIKit ...@@ -12,9 +12,9 @@ import UIKit
struct BlendHealthCenter: Codable { struct BlendHealthCenter: Codable {
public let s2CellID: String public let s2CellID: String
public let updatedOn: Date public let updatedOn: Date
public let airQuality: BlendAirQuality public let airQuality: BlendAirQuality?
public let fire: BlendFire public let fire: BlendFire
public let pollutants, pollen: [BlendPoll] public let pollutants, pollen: [BlendPoll]?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case s2CellID = "s2_cell_id" case s2CellID = "s2_cell_id"
...@@ -22,6 +22,20 @@ struct BlendHealthCenter: Codable { ...@@ -22,6 +22,20 @@ struct BlendHealthCenter: Codable {
case airQuality = "air_quality" case airQuality = "air_quality"
case fire, pollutants, pollen case fire, pollutants, pollen
} }
func toAppModel() -> Health {
let airQuality = self.airQuality?.toAppModel()
let pollutants = self.pollutants?.reduce([String: Pollutant]()) { (dict, blendPoll) in
var dict = dict
if let pollutant = blendPoll.toAppModel() {
dict[pollutant.name] = pollutant
}
return dict
}
let result = Health(lastUpdateTime: updatedOn, airQuality: airQuality, pollutants: pollutants ?? [:])
return result
}
} }
// MARK: - AirQuality // MARK: - AirQuality
...@@ -39,6 +53,13 @@ struct BlendAirQuality: Codable { ...@@ -39,6 +53,13 @@ struct BlendAirQuality: Codable {
case color = "color_code" case color = "color_code"
case imageURL = "image_url" case imageURL = "image_url"
} }
func toAppModel() -> AirQuality {
#warning("Not implemented!")
//TODO: implement health advice groups support (general / sensitive / active)
let result = AirQuality(index: aqi, advice: healthAdvice.general)
return result
}
} }
// MARK: - HealthAdvice // MARK: - HealthAdvice
...@@ -72,6 +93,17 @@ struct BlendPoll: Codable { ...@@ -72,6 +93,17 @@ struct BlendPoll: Codable {
case status case status
case colorCode = "color_code" case colorCode = "color_code"
} }
// Note: the same model in Blend is also used for pollen, which, in my opinion, should be
// a different app-level model object.
func toAppModel() -> Pollutant? {
// Note: pollutants always seem to have a value. Pollen doesn't.
guard let value = value else {
return nil
}
let result = Pollutant(name: name, value: value)
return result
}
} }
struct BlendColor: Codable { struct BlendColor: Codable {
...@@ -85,7 +117,7 @@ struct BlendColor: Codable { ...@@ -85,7 +117,7 @@ struct BlendColor: Codable {
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
guard let hexRepresentation = uiColor.toHex() else { guard let hexRepresentation = uiColor.toHex() else {
throw BlendHealthError.dataEncodingError("BlendColor to HEX conversion failed for color \(uiColor)") throw BlendHealthSourceError.dataEncodingError("BlendColor to HEX conversion failed for color \(uiColor)")
} }
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
try container.encode("#\(hexRepresentation)") try container.encode("#\(hexRepresentation)")
......
...@@ -31,7 +31,6 @@ public class WdtWeatherSource: WeatherSource { ...@@ -31,7 +31,6 @@ public class WdtWeatherSource: WeatherSource {
return queue return queue
}() }()
/// This is used
private var locationsBeingUpdated = Set<Location>() private var locationsBeingUpdated = Set<Location>()
public let weatherUpdateInterval = TimeInterval(15 * 60) // 15 minutes public let weatherUpdateInterval = TimeInterval(15 * 60) // 15 minutes
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment