package org.bensteele.jirrigate.weather.weatherunderground;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import jline.internal.Log;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.bensteele.jirrigate.weather.WeatherStation;
import org.bensteele.jirrigate.weather.weatherunderground.response.Forecastday;
import org.bensteele.jirrigate.weather.weatherunderground.response.WeatherUndergroundForecastResponse;
import org.bensteele.jirrigate.weather.weatherunderground.response.WeatherUndergroundResponse;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
/**
* A WeatherUnderground implementation of a {@link WeatherStation}. Designed to be used with
* Personal Weather Stations (PWS) that upload data to WeatherUnderground. An API account is
* required to get the API key to use this class, the PWS Station ID can be your own or one close by
* that suits.
* <p>
* The {@link WeatherUndergroundResponse} and {@link WeatherUndergroundForecastResponse} that this
* class produces were generated through a simple JSON to POJO generator and not intended for
* modification.
* <p>
* {@link http://www.wunderground.com/weather/api/}
*
* @author Ben Steele (ben@bensteele.org)
*/
public class WeatherUndergroundStation implements WeatherStation {
private final String apiKey;
private final String stationId;
private final String url;
protected final List<WeatherUndergroundResponse> responses = Collections
.synchronizedList(new ArrayList<WeatherUndergroundResponse>());
private final AtomicReference<WeatherUndergroundForecastResponse> forecastResponse = new AtomicReference<WeatherUndergroundForecastResponse>();
private final ExecutorService requestExecutor = Executors.newSingleThreadExecutor();
private final String name;
private final DecimalFormat twoDecimals = new DecimalFormat("###.##");
private boolean isActive;
// Enough for 14 days of responses at 10 minute intervals.
private static final int MAX_RESPONSES_SIZE = 2000;
public WeatherUndergroundStation(String name, String apiKey, String stationId) {
this(name, apiKey, stationId, "http://api.wunderground.com");
}
public WeatherUndergroundStation(final String name, String apiKey, String stationId, String url) {
this.name = name;
this.apiKey = apiKey;
this.stationId = stationId;
this.url = url;
this.isActive = true;
// Fetch new weather data from wunderground every 15 minutes.
requestExecutor.execute(new Runnable() {
@Override
public void run() {
final int SLEEP_30_MINUTES_IN_MS = (1000 * 60) * 30;
while (true) {
try {
if (isActive()) {
if (responses.size() > MAX_RESPONSES_SIZE) {
responses.remove(responses.size() - 1);
}
// Try getting the current data, if that's successful then go for the forecast as
// well.
WeatherUndergroundResponse r = fetchCurrentWeatherData();
if (r != null) {
responses.add(0, r);
WeatherUndergroundForecastResponse fr = fetchForecastWeatherData();
if (fr != null) {
forecastResponse.set(fr);
}
}
}
Thread.sleep(SLEEP_30_MINUTES_IN_MS);
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
}
protected WeatherUndergroundResponse fetchCurrentWeatherData() throws ClientProtocolException,
IOException {
final String wundergroundUrl = url + "/api/" + apiKey + "/conditions/q/pws:" + stationId
+ ".json";
final String response = sendHttpGet(wundergroundUrl);
WeatherUndergroundResponse wundergroundResponse = new WeatherUndergroundResponse();
ObjectMapper mapper = new ObjectMapper();
try {
mapper.readerForUpdating(wundergroundResponse).readValue(response);
} catch (UnrecognizedPropertyException e) {
// TODO (bensteele) log this as we should understand why this is happening.
return null;
}
return wundergroundResponse;
}
protected WeatherUndergroundForecastResponse fetchForecastWeatherData()
throws ClientProtocolException, IOException {
final String wundergroundUrl = url + "/api/" + apiKey + "/forecast10day/q/pws:" + stationId
+ ".json";
final String response = sendHttpGet(wundergroundUrl);
WeatherUndergroundForecastResponse wundergroundResponse = new WeatherUndergroundForecastResponse();
ObjectMapper mapper = new ObjectMapper();
// try {
mapper.readerForUpdating(wundergroundResponse).readValue(response);
// } catch (UnrecognizedPropertyException e) {
// TODO (bensteele) log this as we should understand why this is happening.
// return null;
// }
return wundergroundResponse;
}
@Override
public double getCurrentRelativeHumidityPercentage() {
if (!responses.isEmpty()) {
// wunderground response looks like "24%"
String[] humidity = responses.get(0).getCurrent_observation().getRelative_humidity()
.split("%");
return Double.parseDouble(humidity[0]);
}
return -1000;
}
@Override
public double getCurrentTemperatureCelcius() {
if (!responses.isEmpty()) {
return responses.get(0).getCurrent_observation().getTemp_c().doubleValue();
}
return -1000;
}
@Override
public double getCurrentTemperatureFahrenheit() {
if (!responses.isEmpty()) {
return responses.get(0).getCurrent_observation().getTemp_f().doubleValue();
}
return -1000;
}
@Override
public double getCurrentWindspeedKiloMetresPerHour() {
if (!responses.isEmpty()) {
return responses.get(0).getCurrent_observation().getWind_kph().doubleValue();
}
return -1000;
}
@Override
public double getCurrentWindspeedMilesPerHour() {
if (!responses.isEmpty()) {
return responses.get(0).getCurrent_observation().getWind_mph().doubleValue();
}
return -1000;
}
protected WeatherUndergroundForecastResponse getForecastResponse() {
return forecastResponse.get();
}
@Override
public double getLastXDaysAvgTemperatureCelcius(int days) {
// Don't let a value that can't be supported slip through.
if (days > 14) {
days = 14;
}
DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MMM yyyy");
LocalDate today = new LocalDate();
double totalTemp = 0.0;
int daysFound = 0;
for (int i = 0; i < days; i++) {
double dayTemp = 0.0;
int count = 0;
for (WeatherUndergroundResponse r : responses) {
if (r.getCurrent_observation().getObservation_time_rfc822().contains(fmt.print(today))) {
dayTemp += r.getCurrent_observation().getTemp_c().doubleValue();
count++;
}
}
if (count > 0) {
daysFound++;
totalTemp += (dayTemp / count);
}
today = today.minusDays(1);
}
// Incase we don't have as many results as we are asked to go back we cap it at the days we can
// go back so the average result still looks sane.
if (daysFound < days) {
days = daysFound;
}
double avgTemp = (totalTemp / days);
return Double.parseDouble(twoDecimals.format(avgTemp));
}
@Override
public double getLastXDaysAvgTemperatureFahrenheit(int days) {
// Don't let a value that can't be supported slip through.
if (days > 14) {
days = 14;
}
DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MMM yyyy");
LocalDate today = new LocalDate();
double totalTemp = 0.0;
int daysFound = 0;
for (int i = 0; i < days; i++) {
double dayTemp = 0.0;
int count = 0;
for (WeatherUndergroundResponse r : responses) {
if (r.getCurrent_observation().getObservation_time_rfc822().contains(fmt.print(today))) {
dayTemp += r.getCurrent_observation().getTemp_f().doubleValue();
count++;
}
}
if (count > 0) {
daysFound++;
totalTemp += (dayTemp / count);
}
today = today.minusDays(1);
}
// Incase we don't have as many results as we are asked to go back we cap it at the days we can
// go back so the average result still looks sane.
if (daysFound < days) {
days = daysFound;
}
double avgTemp = (totalTemp / days);
return Double.parseDouble(twoDecimals.format(avgTemp));
}
@Override
public double getLastXDaysRainfallInches(int days) {
// Don't let a value that can't be supported slip through.
if (days > 14) {
days = 14;
}
DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MMM yyyy");
LocalDate today = new LocalDate();
double rainfall = 0.0;
int count = 0;
for (WeatherUndergroundResponse r : responses) {
if (r.getCurrent_observation().getObservation_time_rfc822().contains(fmt.print(today))) {
rainfall += Double.parseDouble(r.getCurrent_observation().getPrecip_today_in());
today = today.minusDays(1);
count++;
}
if (count == days) {
break;
}
}
return rainfall;
}
@Override
public double getLastXDaysRainfallMilliLitres(int days) {
// Don't let a value that can't be supported slip through.
if (days > 14) {
days = 14;
}
DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MMM yyyy");
LocalDate today = new LocalDate();
double rainfall = 0.0;
int count = 0;
for (WeatherUndergroundResponse r : responses) {
if (r.getCurrent_observation().getObservation_time_rfc822().contains(fmt.print(today))) {
rainfall += Double.parseDouble(r.getCurrent_observation().getPrecip_today_metric());
today = today.minusDays(1);
count++;
}
if (count == days) {
break;
}
}
return rainfall;
}
@Override
public double getTodaysMaxTemperatureCelcius() {
double maxTemp = -1000.0;
DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MMM yyyy");
LocalDate today = new LocalDate();
for (WeatherUndergroundResponse r : responses) {
if (r.getCurrent_observation().getObservation_time_rfc822().contains(fmt.print(today))) {
if (r.getCurrent_observation().getTemp_c().doubleValue() > maxTemp) {
maxTemp = r.getCurrent_observation().getTemp_c().doubleValue();
}
}
}
return maxTemp;
}
@Override
public double getTodaysMaxTemperatureFahrenheit() {
double maxTemp = -1000.0;
DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MMM yyyy");
LocalDate today = new LocalDate();
for (WeatherUndergroundResponse r : responses) {
if (r.getCurrent_observation().getObservation_time_rfc822().contains(fmt.print(today))) {
if (r.getCurrent_observation().getTemp_f().doubleValue() > maxTemp) {
maxTemp = r.getCurrent_observation().getTemp_f().doubleValue();
}
}
}
return maxTemp;
}
@Override
public double getTodaysMinTemperatureCelcius() {
double minTemp = 1000.0;
DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MMM yyyy");
LocalDate today = new LocalDate();
for (WeatherUndergroundResponse r : responses) {
if (r.getCurrent_observation().getObservation_time_rfc822().contains(fmt.print(today))) {
if (r.getCurrent_observation().getTemp_c().doubleValue() < minTemp) {
minTemp = r.getCurrent_observation().getTemp_c().doubleValue();
}
}
}
return minTemp;
}
@Override
public double getTodaysMinTemperatureFahrenheit() {
double minTemp = 1000.0;
DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MMM yyyy");
LocalDate today = new LocalDate();
for (WeatherUndergroundResponse r : responses) {
if (r.getCurrent_observation().getObservation_time_rfc822().contains(fmt.print(today))) {
if (r.getCurrent_observation().getTemp_f().doubleValue() < minTemp) {
minTemp = r.getCurrent_observation().getTemp_f().doubleValue();
}
}
}
return minTemp;
}
@Override
public String getName() {
return this.name;
}
@Override
public int getNextXDaysPercentageOfPrecipitation(int days) {
if (forecastResponse.get() == null) {
return 0;
}
// Don't let a value that can't be supported slip through.
if (days > 10) {
days = 10;
}
// The forecast is delivered in periods, a period is half a day (day and night).
int periods = days * 2;
int pop = 0;
for (int i = 0; i < periods; i++) {
for (Forecastday day : forecastResponse.get().getForecast().getTxt_forecast()
.getForecastday()) {
if (day.getPeriod().intValue() == i) {
if (day.getPop().intValue() > pop) {
pop = day.getPop().intValue();
}
}
}
}
return pop;
}
protected List<WeatherUndergroundResponse> getResponses() {
return this.responses;
}
@Override
public String getStatus() {
if (!responses.isEmpty()) {
return responses.get(0).getCurrent_observation().getObservation_time();
} else {
final String wundergroundUrl = url + "/api/" + apiKey + "/conditions/q/pws:" + stationId
+ ".json";
return "No valid responses received from Weather Underground. The URL I am trying is:\n"
+ wundergroundUrl;
}
}
@Override
public double getTodaysRainfallInches() {
if (!responses.isEmpty()) {
return Double.parseDouble(responses.get(0).getCurrent_observation().getPrecip_today_in());
}
return -1000;
}
@Override
public double getTodaysRainfallMilliLitres() {
if (!responses.isEmpty()) {
return Double.parseDouble(responses.get(0).getCurrent_observation().getPrecip_today_metric());
}
return -1000;
}
@Override
public WeatherStationType getType() {
return WeatherStationType.WUNDERGROUND;
}
protected String getUrl() {
return url;
}
@Override
public boolean isActive() {
return this.isActive;
}
/**
* Generic "helper" method to send a HTTP GET to a specified URL.
*
* @param url
* The full URL that you wish to send this request to.
* @return The body of the reply line by line in a List.
* @throws IOException
* If something goes wrong with the request.
*/
private String sendHttpGet(String url) throws ClientProtocolException, IOException {
// Set the timeout to 10 seconds.
final int HTTP_TIMEOUT = 10000;
CloseableHttpClient httpclient = HttpClients
.custom()
.setDefaultRequestConfig(
RequestConfig.custom().setSocketTimeout(HTTP_TIMEOUT)
.setConnectionRequestTimeout(HTTP_TIMEOUT).setConnectTimeout(HTTP_TIMEOUT).build())
.build();
try {
HttpGet httpget = new HttpGet(url);
ResponseHandler<String> responseHandler = new ResponseHandler<String>() {
@Override
public String handleResponse(final HttpResponse response) throws ClientProtocolException,
IOException {
int status = response.getStatusLine().getStatusCode();
if (status >= 200 && status < 300) {
HttpEntity entity = response.getEntity();
return entity != null ? EntityUtils.toString(entity) : null;
} else {
throw new ClientProtocolException("Unexpected response status: " + status);
}
}
};
String responseBody = httpclient.execute(httpget, responseHandler);
return responseBody;
} catch (Exception e) {
Log.error(name
+ " timed out trying to fetch data from Weather Underground; affected URL was: " + url);
return "";
} finally {
httpclient.close();
}
}
@Override
public void setActive(boolean isActive) {
this.isActive = isActive;
}
@Override
public DateTime getNewestRecordTime() {
int epoch = 0;
for (WeatherUndergroundResponse r : responses) {
if (Integer.parseInt(r.getCurrent_observation().getObservation_epoch()) > epoch) {
epoch = Integer.parseInt(r.getCurrent_observation().getObservation_epoch());
}
}
return new DateTime((long) epoch * 1000);
}
@Override
public DateTime getOldestRecordTime() {
int epoch = Integer.MAX_VALUE;
for (WeatherUndergroundResponse r : responses) {
if (Integer.parseInt(r.getCurrent_observation().getObservation_epoch()) < epoch) {
epoch = Integer.parseInt(r.getCurrent_observation().getObservation_epoch());
}
}
return new DateTime((long) epoch * 1000);
}
@Override
public int getNumberOfRecords() {
return this.responses.size();
}
@Override
public double getNextXDaysMaxTempCelcius(int days) {
if (forecastResponse.get() == null) {
return 0.0;
}
// Don't let a value that can't be supported slip through.
if (days > 10) {
days = 10;
}
// The forecast is delivered in periods, a period is half a day (day and night).
int periods = days * 2;
double maxTemp = -1000.0;
for (int i = 0; i < periods; i++) {
for (Forecastday day : forecastResponse.get().getForecast().getSimpleforecast()
.getForecastday()) {
if (day.getPeriod().intValue() == i) {
if (Double.parseDouble(day.getHigh().getCelsius()) > maxTemp) {
maxTemp = Double.parseDouble(day.getHigh().getCelsius());
}
}
}
}
return maxTemp;
}
@Override
public double getNextXDaysMaxTempFahrenheit(int days) {
if (forecastResponse.get() == null) {
return 0.0;
}
// Don't let a value that can't be supported slip through.
if (days > 10) {
days = 10;
}
// The forecast is delivered in periods, a period is half a day (day and night).
int periods = days * 2;
double maxTemp = -1000.0;
for (int i = 0; i < periods; i++) {
for (Forecastday day : forecastResponse.get().getForecast().getSimpleforecast()
.getForecastday()) {
if (day.getPeriod().intValue() == i) {
if (Double.parseDouble(day.getHigh().getFahrenheit()) > maxTemp) {
maxTemp = Double.parseDouble(day.getHigh().getFahrenheit());
}
}
}
}
return maxTemp;
}
}