Commit 1f4dd173 by Demid Merzlyakov

Health: show actual data.

parent 0677389b
...@@ -20,6 +20,7 @@ public class LocationManager { ...@@ -20,6 +20,7 @@ public class LocationManager {
private let deviceLocationMonitor: DeviceLocationMonitor private let deviceLocationMonitor: DeviceLocationMonitor
private let weatherUpdateSource: WeatherSource private let weatherUpdateSource: WeatherSource
private let healthSource: HealthSource
private var defaultLocation = Location(deviceLocation: false, private var defaultLocation = Location(deviceLocation: false,
coordinates: .init(latitude: 37.3230, longitude: -122.0322), // Cupertino coordinates: .init(latitude: 37.3230, longitude: -122.0322), // Cupertino
timeZone: TimeZone(abbreviation: "PST")!) { timeZone: TimeZone(abbreviation: "PST")!) {
...@@ -124,11 +125,12 @@ public class LocationManager { ...@@ -124,11 +125,12 @@ public class LocationManager {
} }
} }
public static let shared = LocationManager(weatherUpdateSource: WdtWeatherSource()) public static let shared = LocationManager(weatherUpdateSource: WdtWeatherSource(), healthSource: BlendHealthSource())
public let maxLocationsCount = 12 public let maxLocationsCount = 12
public init(weatherUpdateSource: WeatherSource) { public init(weatherUpdateSource: WeatherSource, healthSource: HealthSource) {
self.weatherUpdateSource = weatherUpdateSource self.weatherUpdateSource = weatherUpdateSource
self.healthSource = healthSource
self.deviceLocationMonitor = DeviceLocationMonitor() self.deviceLocationMonitor = DeviceLocationMonitor()
self.deviceLocationMonitor.delegate = self self.deviceLocationMonitor.delegate = self
} }
...@@ -138,6 +140,7 @@ public class LocationManager { ...@@ -138,6 +140,7 @@ public class LocationManager {
guard locations.count > 0 else { guard locations.count > 0 else {
log.info("Update all: update default location if needed.") log.info("Update all: update default location if needed.")
updateWeather(for: defaultLocation, updateType: .full) updateWeather(for: defaultLocation, updateType: .full)
updateHealth(for: defaultLocation)
return return
} }
log.info("Update all \(locations.count) locations if needed...") log.info("Update all \(locations.count) locations if needed...")
...@@ -148,6 +151,41 @@ public class LocationManager { ...@@ -148,6 +151,41 @@ public class LocationManager {
if selectedLocation != location { if selectedLocation != location {
updateWeather(for: location, updateType: .preferIncremental) updateWeather(for: location, updateType: .preferIncremental)
} }
updateHealth(for: location)
}
}
public func updateHealth(for location: Location) {
if let lastTimeUpdated = location.health?.lastUpdateTime {
guard Date().timeIntervalSince(lastTimeUpdated) >= healthSource.healthUpdateInterval else {
log.info("Update health (\(location)): fresh enough (last updated at \(lastTimeUpdated)), skip update.")
return
}
}
log.info("Update health for: \(location)")
healthSource.updateHelath(for: location) { [weak self] (health, error) in
guard let self = self else { return }
guard let health = health else {
if let error = error {
self.log.error("Update health (\(location)) error: \(error)")
}
else {
self.log.error("Update health (\(location)) error: unknown error")
}
return
// TODO: we need to somehow track failed attempts, so that we didn't request again and again in case of an error (e.g. server is down).
}
DispatchQueue.main.async {
if let indexToUpdate = self.locations.firstIndex(where: { $0 == location }) {
self.locations[indexToUpdate].health = health
}
else if self.defaultLocation == location {
self.defaultLocation.health = health
}
else {
self.log.warning("Update health: Failed to find location after update. Maybe it was deleted while the update was performed. Maybe something went wrong. Location: \(location)")
}
}
} }
} }
......
...@@ -21,7 +21,13 @@ public class BlendHealthSource: HealthSource { ...@@ -21,7 +21,13 @@ public class BlendHealthSource: HealthSource {
private let log = Logger(componentName: "BlendHealthSource") private let log = Logger(componentName: "BlendHealthSource")
#warning("Not implemented: staging / prod switching!") #warning("Not implemented: staging / prod switching!")
//TODO: 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" private static let healthCenterUrlStaging = "http://sta-1w-dataaggregator.onelouder.com/1weather/api/v1/weather/current"
private static let healthCenterUrlProduction = "https://pro-1w-dataaggregator.onelouder.com/1weather/api/v1/weather/current"
private static var healthCenterUrl: String {
return healthCenterUrlProduction
}
private static let blendAPIKeyHeaderName = "blend-api-key"
private static let blendAPIKey = "0imfnc8mVLWwsAawjYr4Rx-Af50DDqtlx"
public var healthUpdateInterval: TimeInterval = TimeInterval(15 * 60) // 15 minutes public var healthUpdateInterval: TimeInterval = TimeInterval(15 * 60) // 15 minutes
/// This queue is needed to synchronize access to locationsBeingUpdated. Also, to make logging more clear. /// This queue is needed to synchronize access to locationsBeingUpdated. Also, to make logging more clear.
...@@ -60,28 +66,34 @@ public class BlendHealthSource: HealthSource { ...@@ -60,28 +66,34 @@ public class BlendHealthSource: HealthSource {
var queryParameters = [String: String]() var queryParameters = [String: String]()
if let coordinates = location.coordinates { if let coordinates = location.coordinates {
queryParameters["LAT"] = String(format: "%.5f", coordinates.latitude) queryParameters["lat"] = String(format: "%.5f", coordinates.latitude)
queryParameters["LON"] = String(format: "%.5f", coordinates.longitude) queryParameters["lon"] = String(format: "%.5f", coordinates.longitude)
} }
queryParameters["ZIP"] = location.zip queryParameters["zip"] = location.zip
queryParameters["CITY"] = location.cityName queryParameters["city"] = location.cityName
queryParameters["STATE"] = location.region queryParameters["state"] = location.region
queryParameters["COUNTRY"] = location.countryName queryParameters["country"] = location.countryCode
guard !queryParameters.isEmpty else { guard !queryParameters.isEmpty else {
completion(nil, BlendHealthSourceError.insufficientLocationInfo) completion(nil, BlendHealthSourceError.insufficientLocationInfo)
log.error("Not enough information about location.") log.error("Not enough information about location.")
return return
} }
urlComponents.queryItems = queryParameters.map { URLQueryItem(name: $0, value: $1) }
guard let url = urlComponents.url else { guard let url = urlComponents.url else {
completion(nil, BlendHealthSourceError.badUrl) completion(nil, BlendHealthSourceError.badUrl)
return return
} }
log.debug("query params: \(queryParameters)") log.debug("query params: \(queryParameters)")
var request = URLRequest(url: url)
var headers = request.allHTTPHeaderFields ?? [String: String]()
headers[BlendHealthSource.blendAPIKeyHeaderName] = BlendHealthSource.blendAPIKey
request.allHTTPHeaderFields = headers
let urlSession = URLSession.shared let urlSession = URLSession.shared
let dataTask = urlSession.dataTask(with: url) { [weak self] (data, reponse, error) in
guard let self = self else { return } let dataTask = urlSession.dataTask(with: request) { (data, reponse, error) in
// TODO: check response HTTP code
guard let data = data else { guard let data = data else {
completion(nil, BlendHealthSourceError.networkError(error)) completion(nil, BlendHealthSourceError.networkError(error))
return return
......
...@@ -10,14 +10,12 @@ import UIKit ...@@ -10,14 +10,12 @@ import UIKit
// MARK: - HealthCenter // MARK: - HealthCenter
struct BlendHealthCenter: Codable { struct BlendHealthCenter: Codable {
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 updatedOn = "updated_on" case updatedOn = "updated_on"
case airQuality = "air_quality" case airQuality = "air_quality"
case fire, pollutants, pollen case fire, pollutants, pollen
...@@ -33,7 +31,7 @@ struct BlendHealthCenter: Codable { ...@@ -33,7 +31,7 @@ struct BlendHealthCenter: Codable {
return dict return dict
} }
let result = Health(lastUpdateTime: updatedOn, airQuality: airQuality, pollutants: pollutants ?? [:]) let result = Health(lastUpdateTime: Date(), airQuality: airQuality, pollutants: pollutants ?? [:])
return result return result
} }
} }
......
...@@ -40,10 +40,10 @@ class TodayAirQualityCell: UITableViewCell { ...@@ -40,10 +40,10 @@ class TodayAirQualityCell: UITableViewCell {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
public func configure(health: Health) { public func configure(health: Health?) {
airQualityValueLabel.text = "\(Int(health.airQuality?.index ?? 0))" airQualityValueLabel.text = "\(Int(health?.airQuality?.index ?? 0))"
let aqiText = "air.quality.is".localized() let aqiText = "air.quality.is".localized()
let aqiConditionText = health.airQuality?.status.localized ?? "" let aqiConditionText = health?.airQuality?.status.localized ?? ""
let attrString = NSMutableAttributedString(string: "\(aqiText)\n\(aqiConditionText)", let attrString = NSMutableAttributedString(string: "\(aqiText)\n\(aqiConditionText)",
attributes: [.font : AppFont.SFPro.regular(size: 24), attributes: [.font : AppFont.SFPro.regular(size: 24),
.foregroundColor :ThemeManager.currentTheme.secondaryTextColor]) .foregroundColor :ThemeManager.currentTheme.secondaryTextColor])
...@@ -54,7 +54,7 @@ class TodayAirQualityCell: UITableViewCell { ...@@ -54,7 +54,7 @@ class TodayAirQualityCell: UITableViewCell {
//Fill pollutions //Fill pollutions
stackView.removeAll() stackView.removeAll()
health.pollutants.map{$1}.forEach { health?.pollutants.map{$1}.forEach {
let pollutionView = PollutantView() let pollutionView = PollutantView()
pollutionView.configure(pollutant: $0) pollutionView.configure(pollutant: $0)
stackView.addArrangedSubview(pollutionView) stackView.addArrangedSubview(pollutionView)
......
...@@ -40,12 +40,12 @@ class TodayCellFactory: CellFactoryProtocol { ...@@ -40,12 +40,12 @@ class TodayCellFactory: CellFactoryProtocol {
private var todaySection = TodaySection(rows: [.alert, .forecast, .ad, private var todaySection = TodaySection(rows: [.alert, .forecast, .ad,
.conditions, .forecastPeriod, .precipitation, .conditions, .forecastPeriod, .precipitation,
.airQuality, .dayTime, .sun, .moon]) .airQuality, .dayTime, .sun, .moon])
private let health = Health(lastUpdateTime: Date(), // private let health = Health(lastUpdateTime: Date(),
airQuality: .init(index: 48, advice: "some"), // airQuality: .init(index: 48, advice: "some"),
pollutants: ["pm25" : .init(name: "PM 2.5", value: 48), // pollutants: ["pm25" : .init(name: "PM 2.5", value: 48),
"pm10" : .init(name: "PM 10", value: 42), // "pm10" : .init(name: "PM 10", value: 42),
"no2" : .init(name: "NO2", value: 74), // "no2" : .init(name: "NO2", value: 74),
"so2" : .init(name: "SO2", value: 135)]) // "so2" : .init(name: "SO2", value: 135)])
//Public //Public
init(viewModel: TodayViewModel) { init(viewModel: TodayViewModel) {
...@@ -112,7 +112,7 @@ class TodayCellFactory: CellFactoryProtocol { ...@@ -112,7 +112,7 @@ class TodayCellFactory: CellFactoryProtocol {
return cell return cell
case .airQuality: case .airQuality:
let cell = dequeueReusableCell(type: TodayAirQualityCell.self, tableView: tableView, indexPath: indexPath) let cell = dequeueReusableCell(type: TodayAirQualityCell.self, tableView: tableView, indexPath: indexPath)
cell.configure(health: self.health) cell.configure(health: loc.health)
return cell return cell
case .dayTime: case .dayTime:
let cell = dequeueReusableCell(type: TodayDayTimesCell.self, tableView: tableView, indexPath: indexPath) let cell = dequeueReusableCell(type: TodayDayTimesCell.self, tableView: tableView, indexPath: indexPath)
......
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