diff --git a/nbactions.xml b/nbactions.xml index 458eb9a791f33d790612434ca4b4f5c254c57fca..b5de48a593eac5742ccb63fd695fe1ffa5e71bd4 100755 --- a/nbactions.xml +++ b/nbactions.xml @@ -40,6 +40,8 @@ <com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.dump>true</com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.dump> <com.sun.xml.ws.transport.http.HttpAdapter.dump>true</com.sun.xml.ws.transport.http.HttpAdapter.dump> <com.sun.xml.internal.ws.transport.http.HttpAdapter.dump>true</com.sun.xml.internal.ws.transport.http.HttpAdapter.dump> + <no.nibio.vips.logic.weather.FIELDCLIMATE_API_USERNAME>nibiovips</no.nibio.vips.logic.weather.FIELDCLIMATE_API_USERNAME> + <no.nibio.vips.logic.weather.FIELDCLIMATE_API_PASSWORD>q22bspFVPwkaohImV21m</no.nibio.vips.logic.weather.FIELDCLIMATE_API_PASSWORD> diff --git a/src/main/java/no/nibio/vips/util/weather/MetosAPIDataParser.java b/src/main/java/no/nibio/vips/util/weather/MetosAPIDataParser.java new file mode 100644 index 0000000000000000000000000000000000000000..566f69417b09327e20db7d0725cf7952aee2a458 --- /dev/null +++ b/src/main/java/no/nibio/vips/util/weather/MetosAPIDataParser.java @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2017 NIBIO <http://www.nibio.no/>. + * + * This file is part of VIPSLogic. + * VIPSLogic is free software: you can redistribute it and/or modify + * it under the terms of the NIBIO Open Source License as published by + * NIBIO, either version 1 of the License, or (at your option) any + * later version. + * + * VIPSLogic 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 + * NIBIO Open Source License for more details. + * + * You should have received a copy of the NIBIO Open Source License + * along with VIPSLogic. If not, see <http://www.nibio.no/licenses/>. + * + */ + +package no.nibio.vips.util.weather; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.text.MessageFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.TimeZone; +import java.util.logging.Level; +import java.util.logging.Logger; +import no.nibio.vips.entity.WeatherObservation; +import no.nibio.vips.logic.util.SystemTime; +import no.nibio.vips.util.WeatherElements; +import org.apache.http.Header; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.auth.AuthenticationException; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthCache; +import org.apache.http.client.CookieStore; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.cookie.BasicClientCookie; +import org.apache.http.message.BasicNameValuePair; + +/** + * + * Gets data from the Pessl METOS fieldclimate API. + * Read about the API here: http://www.fieldclimate.com/api/intro.html + * + * @copyright 2017 <a href="http://www.nibio.no/">NIBIO</a> + * @author Tor-Einar Skog <tor-einar.skog@nibio.no> + */ +public class MetosAPIDataParser { + public final static String METOS_API_URL_TEMPLATE = "http://www.fieldclimate.com/api/CIDIStationData3/GetFromDate?{0}&group_code=1&dt_from={1}&row_count={2}"; + + // Mappping VIPS parameters and Metos sensors + // Fieldclimate codes should be in decreasing priority + // TODO; Separate parameter: Soil temperature. Search for name "Soil temperature", higher channel number = deeper sensor + private final ParamInfo[] PARAM_MAP = { + new ParamInfo(WeatherElements.PRECIPITATION, new Integer[] {6}, "sum"), + new ParamInfo(WeatherElements.LEAF_WETNESS, new Integer[] {4}, "time"), + new ParamInfo(WeatherElements.GLOBAL_RADIATION, new Integer[] {600}, "avg"), + new ParamInfo(WeatherElements.WIND_SPEED_2M, new Integer[] {5}, "avg"), + new ParamInfo(WeatherElements.TEMPERATURE_MEAN, new Integer[] {16385}, "avg"), + new ParamInfo(WeatherElements.TEMPERATURE_MAXIMUM, new Integer[] {16385}, "max"), + new ParamInfo(WeatherElements.TEMPERATURE_MINIMUM, new Integer[] {16385}, "min") + }; + + public List<WeatherObservation> getWeatherObservations_old(String stationId, TimeZone timeZone, Date startDate) throws ParseWeatherDataException + { + List<WeatherObservation> retVal = new ArrayList<>(); + // TODO: Finn ut stasjonens egen timezone + // http://www.fieldclimate.com/api/CIDIStationConfig2/Get?user_name=XX&user_passw=XX&station_name=XXXXXXXX + // Antar at respons "f_timezone":"120" representerer minuttoffsett fra GMT (så GMT+2 i dette eksempelet) + // Denne settes så i begge SimpleDateFormat-klassene + SimpleDateFormat URLDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + SimpleDateFormat ResponseDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + // Deduce how many rows of data need be returned. Default: From startDate until now + //Date now = SystemTime.getSystemTime(); + // TODO: Fix the TimeZone here!!! + LocalDateTime now = LocalDateTime.ofInstant(SystemTime.getSystemTime().toInstant(), ZoneId.systemDefault()); + LocalDateTime then = LocalDateTime.ofInstant(startDate.toInstant(), ZoneId.systemDefault()); + Long hoursBetween = ChronoUnit.HOURS.between(then, now); + try + { + URL metosAPIURL = new URL(MessageFormat.format(MetosAPIDataParser.METOS_API_URL_TEMPLATE, stationId, URLDateFormat.format(startDate), hoursBetween)); + System.out.println(metosAPIURL.toString()); + } + catch(MalformedURLException ex) + { + throw new ParseWeatherDataException(ex.getMessage()); + } + + + return retVal; + } + + /** + * + * @return + * @throws UnsupportedEncodingException + * @throws IOException + */ + public String getAccessCode() throws UnsupportedEncodingException, IOException + { + String accessCode = null; + + HttpPost httppost = new HttpPost("https://oauth.fieldclimate.com/authorize?response_type=code&client_id=MetosDemo&state=xyz"); + CookieStore cookieStore = new BasicCookieStore(); + BasicClientCookie cookie = new BasicClientCookie("METOS","metos_srv_5"); + cookie.setPath("/"); + cookie.setDomain("oauth.fieldclimate.com"); + cookieStore.addCookie(cookie); + CloseableHttpClient httpclient = HttpClients.custom().setDefaultCookieStore(cookieStore).build(); + // Request parameters and other properties. + List<NameValuePair> params = new ArrayList<>(); + params.add(new BasicNameValuePair("username", System.getProperty("no.nibio.vips.logic.weather.FIELDCLIMATE_API_USERNAME"))); + params.add(new BasicNameValuePair("password", System.getProperty("no.nibio.vips.logic.weather.FIELDCLIMATE_API_PASSWORD"))); + params.add(new BasicNameValuePair("authorization", "true")); + httppost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")); + + //Execute and get the response. + ResponseHandler<String> responseHandler=new BasicResponseHandler(); + HttpResponse response = httpclient.execute(httppost); + /*for(Header h:response.getAllHeaders()) + { + System.out.println(h.getName() + ": " + h.getValue()); + }*/ + Header location = response.getFirstHeader("Location"); + if(location != null && location.getValue().split("code=").length > 1) + { + try + { + Optional<NameValuePair> code = URLEncodedUtils.parse(new URI(location.getValue()), "UTF-8").stream() + .filter(key -> key.getName().equals("code")) + .findFirst(); + accessCode = code.isPresent() ? code.get().getValue(): null; + } + catch(URISyntaxException ex) + { + ex.printStackTrace(); + } + } + //System.out.println("accessCode=" + accessCode); + return accessCode; + } + + public String getToken() throws IOException, AuthenticationException + { + + String accessCode = this.getAccessCode(); + + CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpPost httppost = new HttpPost("https://oauth.fieldclimate.com/token"); + + // Setting up HTTP BASIC authentication + CredentialsProvider provider = new BasicCredentialsProvider(); + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("MetosDemo", "aa8f4b62b72986bac7c84be78836c2c6"); + + HttpHost targetHost = new HttpHost("oauth.fieldclimate.com", 80, "https"); + AuthCache authCache = new BasicAuthCache(); + authCache.put(targetHost, new BasicScheme()); + BasicScheme basicScheme = new BasicScheme(); + final HttpClientContext context = HttpClientContext.create(); + context.setCredentialsProvider(provider); + context.setAuthCache(authCache); + httppost.addHeader(basicScheme.authenticate(credentials, httppost, context)); + + List<NameValuePair> params = new ArrayList<>(); + params.add(new BasicNameValuePair("grant_type","authorization_code")); + params.add(new BasicNameValuePair("code",accessCode)); + httppost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")); + + + HttpResponse response = httpclient.execute(httppost); + BufferedReader s = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); + String line = s.readLine(); + ObjectMapper m = new ObjectMapper(); + JsonNode jn = m.readTree(line); + return jn.findValue("access_token").asText(); + } + + public List<WeatherObservation> getParsedObservations(String jsonTxt, TimeZone timeZone) throws IOException, ParseException + { + + ObjectMapper oMapper = new ObjectMapper(); + JsonNode jNode = oMapper.readTree(jsonTxt); + List<WeatherObservation> retVal = new ArrayList<>(); + + JsonNode dates = jNode.get("dates"); + JsonNode data = jNode.get("data"); + SimpleDateFormat dFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + dFormat.setTimeZone(timeZone); + for(ParamInfo paramInfo:this.PARAM_MAP) + { + boolean foundParam = false; + for(Integer code:paramInfo.preferredCodes) + { + for(JsonNode aData:data) + { + if(aData.get("code").asInt() == code) + { + for(int i=0;i< dates.size();i++) + { + WeatherObservation obs = new WeatherObservation(); + obs.setTimeMeasured(dFormat.parse(dates.get(i).asText())); + obs.setElementMeasurementTypeId(paramInfo.VIPSCode); + obs.setValue(aData.get("aggr").get(paramInfo.aggregationType).get(i).asDouble()); + obs.setLogIntervalId(WeatherObservation.LOG_INTERVAL_ID_1H); + retVal.add(obs); + //System.out.println(obs.toString()); + } + foundParam = true; + } + if(foundParam) + { + break; + } + } + if(foundParam) + { + break; + } + } + } + + return retVal; + } + + + public List<WeatherObservation> getWeatherObservations(String stationId, TimeZone timeZone, Date startDate) throws ParseWeatherDataException + { + try + { + String accessToken = this.getToken(); + HttpGet httpget = new HttpGet("https://api.fieldclimate.com/v1/data/optimized/000024A0/hourly/from/" + (startDate.getTime() / 1000)); + + httpget.addHeader("Accept", "application/json"); + httpget.addHeader("Authorization", "Bearer " + accessToken); + CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpResponse response = httpclient.execute(httpget); + BufferedReader s = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); + String all = ""; + String line; + while((line = s.readLine()) != null) + { + all += line; + //System.out.println("Linje"); + } + + return this.getParsedObservations(all, timeZone); // TODO TimeZone + } + catch(IOException | ParseException | AuthenticationException ex) + { + throw new ParseWeatherDataException(ex.getMessage()); + } + } + + /** + * Data structure for easy parameter mapping + */ + private class ParamInfo { + + public final String VIPSCode; + public final Integer[] preferredCodes; + public final String aggregationType; + public ParamInfo(String VIPSCode, Integer[] preferredCodes, String aggregationType) + { + this.VIPSCode = VIPSCode; + this.preferredCodes = preferredCodes; + this.aggregationType = aggregationType; + } + + } + + /** + * For hacking/experimentation + * @throws IOException + * @throws AuthenticationException + */ + public void testAPI() throws IOException, AuthenticationException + { + + String accessToken = this.getToken(); + //HttpGet httpget = new HttpGet("https://api.fieldclimate.com/station/000024A0"); + //HttpGet httpget = new HttpGet("https://api.fieldclimate.com/v1/data/optimized/000024A0/hourly/last/5d"); + Calendar cal = Calendar.getInstance(); + cal.set(2017, Calendar.AUGUST, 1, 0, 0, 0); + HttpGet httpget = new HttpGet("https://api.fieldclimate.com/v1/data/optimized/000024A0/hourly/from/" + (cal.getTime().getTime()/1000)); + //HttpGet httpget = new HttpGet("https://api.fieldclimate.com/v1/system/group/sensors"); + httpget.addHeader("Accept", "application/json"); + httpget.addHeader("Authorization", "Bearer " + accessToken); + CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpResponse response = httpclient.execute(httpget); + BufferedReader s = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); + String all = ""; + String line = null; + while((line = s.readLine()) != null) + { + all += line; + //System.out.println("Linje"); + } + + + + ObjectMapper oMapper = new ObjectMapper(); + JsonNode jNode = oMapper.readTree(all); + //Iterator keys = jNode.fieldNames(); + /*while(keys.hasNext()) + { + System.out.println(keys.next()); + } + + JsonNode dates = jNode.get("dates"); + JsonNode data = jNode.get("data"); + for(int i=0;i< dates.size();i++) + { + //System.out.println(dates.get(i).asText() + ": " + data.get("1_X_X_600").get("name").asText() + " = " + data.get("1_X_X_600").get("aggr").get("avg").get(i).asDouble()); + }*/ + try { + this.getParsedObservations(all, TimeZone.getTimeZone("Europe/Vilnius")); + } catch (ParseException ex) { + Logger.getLogger(MetosAPIDataParser.class.getName()).log(Level.SEVERE, null, ex); + } + System.out.println(all); + } +} \ No newline at end of file diff --git a/src/test/java/no/nibio/vips/util/weather/MetosAPIDataParserTest.java b/src/test/java/no/nibio/vips/util/weather/MetosAPIDataParserTest.java new file mode 100644 index 0000000000000000000000000000000000000000..78f553258053142f9236135f1bd523c8acd7176c --- /dev/null +++ b/src/test/java/no/nibio/vips/util/weather/MetosAPIDataParserTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2017 NIBIO <http://www.nibio.no/>. + * + * This file is part of VIPSLogic. + * VIPSLogic is free software: you can redistribute it and/or modify + * it under the terms of the NIBIO Open Source License as published by + * NIBIO, either version 1 of the License, or (at your option) any + * later version. + * + * VIPSLogic 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 + * NIBIO Open Source License for more details. + * + * You should have received a copy of the NIBIO Open Source License + * along with VIPSLogic. If not, see <http://www.nibio.no/licenses/>. + * + */ +package no.nibio.vips.util.weather; + + +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; +import java.util.logging.Level; +import java.util.logging.Logger; +import no.nibio.vips.entity.WeatherObservation; +import org.apache.http.auth.AuthenticationException; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author treinar + */ +public class MetosAPIDataParserTest { + + public MetosAPIDataParserTest() { + } + + @BeforeClass + public static void setUpClass() { + + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() throws NoSuchAlgorithmException, KeyStoreException { + + } + + @After + public void tearDown() { + } + + /** + * Test of getWeatherObservations method, of class MetosAPIDataParser. + */ + @Test + public void testGetWeatherObservations() throws Exception { + System.out.println("getWeatherObservations"); + String stationId = "000024A0"; + TimeZone timeZone = TimeZone.getTimeZone("Europe/Vilnius"); + Calendar cal = Calendar.getInstance(); + cal.setTimeZone(timeZone); + cal.set(2017,Calendar.MAY,20,0,0,0); + Date startDate = cal.getTime(); + MetosAPIDataParser instance = new MetosAPIDataParser(); + List<WeatherObservation> result = instance.getWeatherObservations(stationId, timeZone, startDate); + assertNotNull( result); + //result.stream().forEach(obs -> System.out.println(obs.toString())); + + } + + /** + * Test of getAccessCode method, of class MetosAPIDataParser. + */ + //@Test + public void testGetAccessCode() { + try { + System.out.println("getAccessCode"); + MetosAPIDataParser instance = new MetosAPIDataParser(); + String expResult = ""; + String result = instance.getAccessCode(); + assertNotNull(result); + + } catch (IOException ex) { + Logger.getLogger(MetosAPIDataParserTest.class.getName()).log(Level.SEVERE, null, ex); + } + } + + //@Test + public void testGetToken() + { + System.out.println("getToken"); + try { + MetosAPIDataParser instance = new MetosAPIDataParser(); + + String result = instance.getToken(); + //System.out.println("access_token = " + result); + assertNotNull(result); + } catch (IOException | AuthenticationException ex) { + Logger.getLogger(MetosAPIDataParserTest.class.getName()).log(Level.SEVERE, null, ex); + } + } + + //@Test + public void testAPI(){ + System.out.println("testAPI"); + MetosAPIDataParser instance = new MetosAPIDataParser(); + try { + instance.testAPI(); + } catch (IOException ex) { + fail(ex.getMessage()); + } catch (AuthenticationException ex) { + Logger.getLogger(MetosAPIDataParserTest.class.getName()).log(Level.SEVERE, null, ex); + } + } + +}