/* ==================================================================
* BasicMetserviceClient.java - 28/05/2016 1:37:46 pm
*
* Copyright 2007-2016 SolarNetwork.net Dev Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
*/
package net.solarnetwork.node.weather.nz.metservice;
import static net.solarnetwork.util.JsonNodeUtils.parseBigDecimalAttribute;
import static net.solarnetwork.util.JsonNodeUtils.parseDateAttribute;
import static net.solarnetwork.util.JsonNodeUtils.parseIntegerAttribute;
import static net.solarnetwork.util.JsonNodeUtils.parseStringAttribute;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URLConnection;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import org.joda.time.LocalTime;
import org.springframework.util.FileCopyUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.solarnetwork.node.domain.GeneralAtmosphericDatum;
import net.solarnetwork.node.domain.GeneralDayDatum;
import net.solarnetwork.node.domain.GeneralLocationDatum;
import net.solarnetwork.node.support.HttpClientSupport;
import net.solarnetwork.node.support.UnicodeReader;
/**
* Basic implementation of {@link MetserviceClient}.
*
* @author matt
* @version 1.1
*/
public class BasicMetserviceClient extends HttpClientSupport implements MetserviceClient {
/** The default value for the {@code baseUrl} property. */
public static final String DEFAULT_BASE_URL = "http://www.metservice.com/publicData";
/** The default value for the {@code locationKey} property. */
public static final String DEFAULT_LOCATION_KEY = "wellington-city";
/** The default value for the {@code localObsTemplate} property. */
public static final String DEFAULT_LOCAL_OBS_SET_TEMPLATE = "localObs_%s";
/** The default value for the {@code localForecastTemplate} property. */
public static final String DEFAULT_LOCAL_FORECAST_SET_TEMPLATE = "localForecast%s";
/** The default value for the {@code oneMinObsTemplate} property. */
public static final String DEFAULT_ONE_MINUTE_OBS_SET_TEMPLATE = "oneMinuteObs_%s";
/** The default value for the {@code riseSetTemplate} property. */
public static final String DEFAULT_RISE_SET_TEMPLATE = "riseSet_%s";
/**
* The default value for the {@code hourlyObsAndForecastTemplate} property.
*/
public static final String DEFAULT_HOURLY_OBS_AND_FORECAST_TEMPLATE = "hourlyObsAndForecast_%s";
/** The default value for the {@code dayDateFormat} property. */
public static final String DEFAULT_DAY_DATE_FORMAT = "d MMMM yyyy";
/** The default value for the {@code timestampHourDateFormat} property. */
public static final String DEFAULT_TIMESTAMP_HOUR_DATE_FORMAT = "HH:mm EE d MMMM yyyy";
/** The default value for the {@code timeDateFormat} property. */
public static final String DEFAULT_TIME_DATE_FORMAT = "h:mma";
/** The default value for the {@code timestampDateFormat} property. */
public static final String DEFAULT_TIMESTAMP_DATE_FORMAT = "h:mma EEEE d MMM yyyy";
/** The default value for the {@code timeZoneId} property. */
public static final String DEFAULT_TIME_ZONE_ID = "Pacific/Auckland";
private String baseUrl;
private String localObsTemplate;
private String oneMinuteObsTemplate;
private String localForecastTemplate;
private String hourlyObsAndForecastTemplate;
private String riseSetTemplate;
private String dayDateFormat;
private String timeDateFormat;
private String timestampDateFormat;
private String timestampHourDateFormat;
private String timeZoneId;
private ObjectMapper objectMapper;
/**
* Default constructor.
*/
public BasicMetserviceClient() {
super();
baseUrl = DEFAULT_BASE_URL;
localObsTemplate = DEFAULT_LOCAL_OBS_SET_TEMPLATE;
localForecastTemplate = DEFAULT_LOCAL_FORECAST_SET_TEMPLATE;
oneMinuteObsTemplate = DEFAULT_ONE_MINUTE_OBS_SET_TEMPLATE;
hourlyObsAndForecastTemplate = DEFAULT_HOURLY_OBS_AND_FORECAST_TEMPLATE;
riseSetTemplate = DEFAULT_RISE_SET_TEMPLATE;
dayDateFormat = DEFAULT_DAY_DATE_FORMAT;
timestampHourDateFormat = DEFAULT_TIMESTAMP_HOUR_DATE_FORMAT;
timeDateFormat = DEFAULT_TIME_DATE_FORMAT;
timestampDateFormat = DEFAULT_TIMESTAMP_DATE_FORMAT;
timeZoneId = DEFAULT_TIME_ZONE_ID;
}
private String getURLForLocationTemplate(String template, String locationKey) {
return getBaseUrl() + '/' + String.format(template, locationKey);
}
@Override
public GeneralDayDatum readCurrentRiseSet(final String locationKey) {
if ( locationKey == null ) {
return null;
}
final String url = getURLForLocationTemplate(getRiseSetTemplate(), locationKey);
final SimpleDateFormat timeFormat = new SimpleDateFormat(getTimeDateFormat());
final SimpleDateFormat dayFormat = new SimpleDateFormat(getDayDateFormat());
dayFormat.setCalendar(Calendar.getInstance(TimeZone.getTimeZone(getTimeZoneId())));
GeneralDayDatum result = null;
try {
URLConnection conn = getURLConnection(url, HTTP_METHOD_GET);
JsonNode data = getObjectMapper().readTree(getInputStreamFromURLConnection(conn));
result = parseRiseSet(data, dayFormat, timeFormat);
} catch ( IOException e ) {
log.warn("Error reading MetService URL [{}]: {}", url, e.getMessage());
}
return result;
}
private GeneralDayDatum parseRiseSet(JsonNode data, SimpleDateFormat dayFormat,
SimpleDateFormat timeFormat) {
if ( data == null ) {
return null;
}
GeneralDayDatum result = null;
Date day = parseDateAttribute(data, "day", dayFormat);
Date sunrise = parseDateAttribute(data, "sunRise", timeFormat);
Date sunset = parseDateAttribute(data, "sunSet", timeFormat);
Date moonrise = parseDateAttribute(data, "moonRise", timeFormat);
Date moonset = parseDateAttribute(data, "moonSet", timeFormat);
if ( day != null && sunrise != null && sunset != null ) {
result = new GeneralDayDatum();
result.setCreated(day);
result.setSunrise(new LocalTime(sunrise));
result.setSunset(new LocalTime(sunset));
if ( moonrise != null ) {
result.setMoonrise(new LocalTime(moonrise));
}
if ( moonset != null ) {
result.setMoonset(new LocalTime(moonset));
}
log.debug("Parsed DayDatum from rise set: {}", result);
}
return result;
}
@Override
public Collection<GeneralLocationDatum> readCurrentLocalObservations(String locationKey) {
if ( locationKey == null ) {
return null;
}
final List<GeneralLocationDatum> result = new ArrayList<GeneralLocationDatum>(4);
final String url = getURLForLocationTemplate(getLocalObsTemplate(), locationKey);
final SimpleDateFormat tsFormat = new SimpleDateFormat(getTimestampDateFormat());
tsFormat.setCalendar(Calendar.getInstance(TimeZone.getTimeZone(getTimeZoneId())));
JsonNode root;
try {
URLConnection conn = getURLConnection(url, HTTP_METHOD_GET);
root = getObjectMapper().readTree(getInputStreamFromURLConnection(conn));
} catch ( IOException e ) {
log.warn("Error reading MetService URL [{}]: {}", url, e.getMessage());
return result;
}
JsonNode data = root.get("threeHour");
if ( data == null ) {
log.warn("Local observation container key 'threeHour' not found in {}", url);
} else {
Date infoDate = parseDateAttribute(data, "dateTime", tsFormat);
BigDecimal temp = parseBigDecimalAttribute(data, "temp");
if ( infoDate == null || temp == null ) {
log.debug("Date and/or temperature missing from key 'threeHour' in {}", url);
} else {
GeneralAtmosphericDatum weather = new GeneralAtmosphericDatum();
weather.setCreated(infoDate);
weather.setTemperature(temp);
weather.setHumidity(parseIntegerAttribute(data, "humidity"));
BigDecimal millibar = parseBigDecimalAttribute(data, "pressure");
if ( millibar != null ) {
int pascals = (millibar.multiply(new BigDecimal(100))).intValue();
weather.setAtmosphericPressure(pascals);
}
// TODO: rainfall?
result.add(weather);
}
}
data = root.get("twentyFourHour");
if ( data == null ) {
log.warn("Local observation container key 'twentyFourHour' not found in {}", url);
} else {
Date infoDate = parseDateAttribute(data, "dateTime", tsFormat);
BigDecimal maxTemp = parseBigDecimalAttribute(data, "maxTemp");
BigDecimal minTemp = parseBigDecimalAttribute(data, "minTemp");
if ( infoDate == null || minTemp == null || maxTemp == null ) {
log.debug("Date and/or temperature extremes missing from key 'twentyFourHour' in {}",
url);
} else {
GeneralDayDatum day = new GeneralDayDatum();
day.setCreated(infoDate);
day.setTemperatureMinimum(minTemp);
day.setTemperatureMaximum(maxTemp);
// TODO: rainfall?
result.add(day);
}
}
return result;
}
@Override
public Collection<GeneralDayDatum> readLocalForecast(String locationKey) {
// get local forecast
final String url = getURLForLocationTemplate(getLocalForecastTemplate(), locationKey);
final List<GeneralDayDatum> result = new ArrayList<GeneralDayDatum>(4);
final SimpleDateFormat timeFormat = new SimpleDateFormat(getTimeDateFormat());
final SimpleDateFormat dayFormat = new SimpleDateFormat(getDayDateFormat());
dayFormat.setCalendar(Calendar.getInstance(TimeZone.getTimeZone(getTimeZoneId())));
JsonNode root;
try {
URLConnection conn = getURLConnection(url, HTTP_METHOD_GET);
root = getObjectMapper().readTree(getInputStreamFromURLConnection(conn));
} catch ( IOException e ) {
log.warn("Error reading MetService URL [{}]: {}", url, e.getMessage());
return result;
}
JsonNode days = root.get("days");
if ( days.isArray() ) {
for ( JsonNode dayNode : days ) {
GeneralDayDatum day = parseRiseSet(dayNode.get("riseSet"), dayFormat, timeFormat);
if ( day != null ) {
day.setSkyConditions(parseStringAttribute(dayNode, "forecastWord"));
day.setTemperatureMinimum(parseBigDecimalAttribute(dayNode, "min"));
day.setTemperatureMaximum(parseBigDecimalAttribute(dayNode, "max"));
result.add(day);
}
}
}
return result;
}
@Override
public Collection<GeneralAtmosphericDatum> readHourlyForecast(String locationKey) {
final String url = getURLForLocationTemplate(getHourlyObsAndForecastTemplate(), locationKey);
final List<GeneralAtmosphericDatum> result = new ArrayList<GeneralAtmosphericDatum>(4);
final SimpleDateFormat hourTimestampFormat = new SimpleDateFormat(getTimestampHourDateFormat());
hourTimestampFormat.setCalendar(Calendar.getInstance(TimeZone.getTimeZone(getTimeZoneId())));
JsonNode root;
try {
URLConnection conn = getURLConnection(url, HTTP_METHOD_GET);
root = getObjectMapper().readTree(getInputStreamFromURLConnection(conn));
} catch ( IOException e ) {
log.warn("Error reading MetService URL [{}]: {}", url, e.getMessage());
return result;
}
JsonNode hours = root.get("forecastData");
if ( hours.isArray() ) {
for ( JsonNode hourNode : hours ) {
String time = parseStringAttribute(hourNode, "timeFrom");
String date = parseStringAttribute(hourNode, "date");
BigDecimal temp = parseBigDecimalAttribute(hourNode, "temperature");
Date infoDate = null;
if ( time != null && date != null ) {
String dateString = time + " " + date;
try {
infoDate = hourTimestampFormat.parse(dateString);
} catch ( ParseException e ) {
log.debug(
"Error parsing date attribute [timeFrom date] value [{}] using pattern {}: {}",
new Object[] { dateString, hourTimestampFormat.toPattern(),
e.getMessage() });
}
}
if ( infoDate == null || temp == null ) {
continue;
}
GeneralAtmosphericDatum weather = new GeneralAtmosphericDatum();
weather.setCreated(infoDate);
weather.setTemperature(temp);
result.add(weather);
}
}
return result;
}
public String getBaseUrl() {
return baseUrl;
}
/**
* Read an InputStream as Unicode text and return as a String.
*
* @param in
* the InputStream to read
* @return the text
* @throws IOException
* if an IO error occurs
*/
protected String readUnicodeInputStream(InputStream in) throws IOException {
UnicodeReader reader = new UnicodeReader(in, null);
String data = FileCopyUtils.copyToString(reader);
reader.close();
return data;
}
/**
* The base URL for queries to MetService. Defaults to
* {@link #DEFAULT_BASE_URL}.
*
* @param baseUrl
* The base URL to use.
*/
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public ObjectMapper getObjectMapper() {
return objectMapper;
}
/**
* Set the {@link ObjectMapper} to use for parsing JSON.
*
* @param objectMapper
* The object mapper.
*/
public void setObjectMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public String getLocalObsTemplate() {
return localObsTemplate;
}
/**
* The name of the "localObs" file to parse, using a single string parameter
* for the location key. This file is expected to contain a single JSON
* object declaration with the humidity, pressure, etc. attributes. Defaults
* to {@link #DEFAULT_LOCAL_OBS_SET_TEMPLATE}.
*
* @param localObsTemplate
* The file name template to use.
*/
public void setLocalObsTemplate(String localObsTemplate) {
this.localObsTemplate = localObsTemplate;
}
public String getLocalForecastTemplate() {
return localForecastTemplate;
}
/**
* The name of the "localForecast" file to parse, using a single string
* parameter for the location key. This file is expected to contain a single
* JSON object declaration with an array of day JSON objects, the first day
* from which the sky conditions are extracted. The real-time data doesn't
* provide sky conditions, so we just use the presumably static value for
* the day. Defaults to {@link #DEFAULT_LOCAL_FORECAST_SET_TEMPLATE}.
*
* @param localForecastTemplate
* The file name template to use.
*/
public void setLocalForecastTemplate(String localForecastTemplate) {
this.localForecastTemplate = localForecastTemplate;
}
public String getRiseSetTemplate() {
return riseSetTemplate;
}
/**
* The name of the "riseSet" file to parse, using a single string parameter
* for the location key. This file is expected to contain a single JSON
* object declaration with the sunrise, sunset, and date attributes.
* Defaults to {@link #DEFAULT_RISE_SET_TEMPLATE}.
*
* @param riseSetTemplate
* The file name template to use.
*/
public void setRiseSetTemplate(String riseSetTemplate) {
this.riseSetTemplate = riseSetTemplate;
}
public String getOneMinuteObsTemplate() {
return oneMinuteObsTemplate;
}
/**
* The name of the "oneMinuteObs" file to parse, using a single string
* parameter for the location key. This file is expected to contain a single
* JSON object declaration with the weather date attributes. Defaults to
* {@link #DEFAULT_ONE_MINUTE_OBS_SET_TEMPLATE}.
*
* @param oneMinuteObsTemplate
*/
public void setOneMinuteObsTemplate(String oneMinuteObsTemplate) {
this.oneMinuteObsTemplate = oneMinuteObsTemplate;
}
public String getHourlyObsAndForecastTemplate() {
return hourlyObsAndForecastTemplate;
}
/**
* The name of the "hourlyObsAndForecast" file to parse, using a single
* string parameter for the location key. Defaults to
* {@link #DEFAULT_ONE_MINUTE_OBS_SET_TEMPLATE}.
*
* @param oneMinuteObsTemplate
*/
public void setHourlyObsAndForecastTemplate(String hourlyObsAndForecastTemplate) {
this.hourlyObsAndForecastTemplate = hourlyObsAndForecastTemplate;
}
public String getDayDateFormat() {
return dayDateFormat;
}
/**
* The {@link SimpleDateFormat} date format to use to parse the day date.
* Defaluts to {@link #DEFAULT_DAY_DATE_FORMAT}.
*
* @param dayDateFormat
* The date format to use.
*/
public void setDayDateFormat(String dayDateFormat) {
this.dayDateFormat = dayDateFormat;
}
public String getTimeDateFormat() {
return timeDateFormat;
}
/**
* Set a {@link SimpleDateFormat} time format to use to parse sunrise/sunset
* times. Defaults to {@link #DEFAULT_TIME_DATE_FORMAT}.
*
* @param timeDateFormat
* The date format to use.
*/
public void setTimeDateFormat(String timeDateFormat) {
this.timeDateFormat = timeDateFormat;
}
public String getTimestampDateFormat() {
return timestampDateFormat;
}
/**
* Set a {@link SimpleDateFormat} date and time pattern for parsing the
* information date from the {@code oneMinObs} file. Defaults to
* {@link #DEFAULT_TIMESTAMP_DATE_FORMAT}.
*
* @param timestampDateFormat
* The date format to use.
*/
public void setTimestampDateFormat(String timestampDateFormat) {
this.timestampDateFormat = timestampDateFormat;
}
public String getTimestampHourDateFormat() {
return timestampHourDateFormat;
}
/**
* Set a {@link SimpleDateFormat} date and time pattern for parsing the
* information date from the {@code hourlyObsAndForecast} file. Defaults to
* {@link #DEFAULT_TIMESTAMP_HOUR_DATE_FORMAT}.
*
* @param timestampHourDateFormat
* The date format to use.
*/
public void setTimestampHourDateFormat(String timestampHourDateFormat) {
this.timestampHourDateFormat = timestampHourDateFormat;
}
public String getTimeZoneId() {
return timeZoneId;
}
/**
* Set the time zone ID used for parsing date strings. Defaults to
* {@link #DEFAULT_TIME_ZONE_ID}.
*
* @param timeZoneId
* The time zone ID to use.
*/
public void setTimeZoneId(String timeZoneId) {
this.timeZoneId = timeZoneId;
}
}