-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
19 changed files
with
1,508 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"User": "[email protected]", | ||
"Password": "InnCOboMJghBLWaL97uD", | ||
"Broker": "tcp://kube.gruning.eu:1883", | ||
"aWATTarPrices": true, | ||
"Debug": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
<groupId>eu.gruning</groupId> | ||
<artifactId>discofox</artifactId> | ||
<version>0.0.3-SNAPSHOT</version> | ||
<name>Discofox</name> | ||
<description>Tooling around the Discovergy Smartmeter API</description> | ||
<build> | ||
<sourceDirectory>src</sourceDirectory> | ||
<plugins> | ||
<plugin> | ||
<artifactId>maven-compiler-plugin</artifactId> | ||
<version>3.8.0</version> | ||
<configuration> | ||
<source>1.8</source> | ||
<target>1.8</target> | ||
</configuration> | ||
</plugin> | ||
<plugin> | ||
<artifactId>maven-assembly-plugin</artifactId> | ||
<configuration> | ||
<archive> | ||
<manifest> | ||
<mainClass>eu.gruning.discofox.main.Discofox</mainClass> | ||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries> | ||
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries> | ||
</manifest> | ||
</archive> | ||
<descriptorRefs> | ||
<descriptorRef>jar-with-dependencies</descriptorRef> | ||
</descriptorRefs> | ||
</configuration> | ||
<executions> | ||
<execution> | ||
<id>make-assembly</id> <!-- this is used for inheritance merges --> | ||
<phase>package</phase> <!-- bind to the packaging phase --> | ||
<goals> | ||
<goal>single</goal> | ||
</goals> | ||
</execution> | ||
</executions> | ||
</plugin> | ||
</plugins> | ||
</build> | ||
<properties> | ||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
</properties> | ||
<dependencies> | ||
<dependency> | ||
<groupId>com.github.scribejava</groupId> | ||
<artifactId>scribejava-core</artifactId> | ||
<version>6.9.0</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>com.google.code.gson</groupId> | ||
<artifactId>gson</artifactId> | ||
<version>2.8.6</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.apache.logging.log4j</groupId> | ||
<artifactId>log4j-core</artifactId> | ||
<version>2.13.0</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.eclipse.paho</groupId> | ||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId> | ||
<version>1.2.2</version> | ||
</dependency> | ||
</dependencies> | ||
</project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package eu.gruning.discofox.apiclient; | ||
|
||
import static java.nio.charset.StandardCharsets.UTF_8; | ||
|
||
import java.io.UnsupportedEncodingException; | ||
import java.net.URLEncoder; | ||
|
||
import com.github.scribejava.core.builder.api.DefaultApi10a; | ||
import com.github.scribejava.core.model.OAuth1RequestToken; | ||
|
||
// This code is based on sample code provided by Discovergy GmbH at https://api.discovergy.com/docs/ | ||
// Thanks Discovergy for providing this including a well documented API | ||
|
||
public class DiscovergyApi extends DefaultApi10a { | ||
|
||
private final String baseAddress; | ||
private final String user; | ||
private final String password; | ||
|
||
public DiscovergyApi(String user, String password) { | ||
this("https://api.discovergy.com/public/v1", user, password); | ||
} | ||
|
||
public DiscovergyApi(String baseAddress, String user, String password) { | ||
this.baseAddress = baseAddress; | ||
this.user = user; | ||
this.password = password; | ||
} | ||
|
||
public String getBaseAddress() { | ||
return baseAddress; | ||
} | ||
|
||
public String getUser() { | ||
return user; | ||
} | ||
|
||
@Override | ||
public String getRequestTokenEndpoint() { | ||
return baseAddress + "/oauth1/request_token"; | ||
} | ||
|
||
@Override | ||
public String getAccessTokenEndpoint() { | ||
return baseAddress + "/oauth1/access_token"; | ||
} | ||
|
||
@Override | ||
public String getAuthorizationBaseUrl() { | ||
return baseAddress + "/oauth1/authorize"; | ||
} | ||
|
||
@Override | ||
public String getAuthorizationUrl(OAuth1RequestToken requestToken) { | ||
try { | ||
return baseAddress + "/oauth1/authorize?oauth_token=" + requestToken.getToken() + "&email=" | ||
+ URLEncoder.encode(user, UTF_8.name()) + "&password=" + URLEncoder.encode(password, UTF_8.name()); | ||
} catch (UnsupportedEncodingException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
} |
138 changes: 138 additions & 0 deletions
138
src/eu/gruning/discofox/apiclient/DiscovergyApiClient.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
package eu.gruning.discofox.apiclient; | ||
|
||
import static java.nio.charset.CodingErrorAction.REPORT; | ||
import static java.nio.charset.StandardCharsets.UTF_8; | ||
|
||
import java.io.File; | ||
import java.io.FileInputStream; | ||
import java.io.IOException; | ||
import java.io.InputStreamReader; | ||
import java.io.Reader; | ||
import java.net.HttpURLConnection; | ||
import java.net.URL; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.Map; | ||
import java.util.Properties; | ||
import java.util.concurrent.ExecutionException; | ||
|
||
import com.github.scribejava.core.builder.ServiceBuilder; | ||
import com.github.scribejava.core.model.OAuth1AccessToken; | ||
import com.github.scribejava.core.model.OAuth1RequestToken; | ||
import com.github.scribejava.core.model.OAuthRequest; | ||
import com.github.scribejava.core.model.Response; | ||
import com.github.scribejava.core.model.Verb; | ||
import com.github.scribejava.core.oauth.OAuth10aService; | ||
import com.github.scribejava.core.utils.StreamUtils; | ||
import com.google.gson.Gson; | ||
|
||
/** | ||
* Client for the Discovergy API (<a href= | ||
* "https://api.discovergy.com/docs/">https://api.discovergy.com/docs/</a>) | ||
*/ | ||
public class DiscovergyApiClient { | ||
|
||
/** | ||
* Unique client id | ||
*/ | ||
private final String clientId; | ||
|
||
private final DiscovergyApi api; | ||
|
||
private final OAuth10aService authenticationService; | ||
private final OAuth1AccessToken accessToken; | ||
|
||
public DiscovergyApiClient(String clientId) throws InterruptedException, ExecutionException, IOException { | ||
this(createDiscovergyApi(), clientId); | ||
} | ||
|
||
public DiscovergyApiClient(DiscovergyApi api, String clientId) | ||
throws InterruptedException, ExecutionException, IOException { | ||
this.api = api; | ||
this.clientId = clientId; | ||
Map<String, String> consumerTokenEntries = getConsumerToken(); | ||
authenticationService = new ServiceBuilder(consumerTokenEntries.get("key")) | ||
.apiSecret(consumerTokenEntries.get("secret")).build(api); | ||
OAuth1RequestToken requestToken = authenticationService.getRequestToken(); | ||
String authorizationURL = authenticationService.getAuthorizationUrl(requestToken); | ||
String verifier = authorize(authorizationURL); | ||
accessToken = authenticationService.getAccessToken(requestToken, verifier); | ||
} | ||
|
||
private static DiscovergyApi createDiscovergyApi() throws IOException { | ||
File file = new File("credentials.properties").getAbsoluteFile(); | ||
Properties properties = new Properties(); | ||
try (Reader reader = new InputStreamReader(new FileInputStream(file), | ||
UTF_8.newDecoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT))) { | ||
properties.load(reader); | ||
} catch (IOException e) { | ||
throw new IOException("Failed to read credentials from file " + file, e); | ||
} | ||
String email = properties.getProperty("email"); | ||
String password = properties.getProperty("password"); | ||
if (email == null || email.isEmpty() || password == null || password.isEmpty()) { | ||
throw new RuntimeException("The properties \"email\" and \"password\" must be set in file " + file); | ||
} | ||
return new DiscovergyApi(email, password); | ||
} | ||
|
||
public DiscovergyApi getApi() { | ||
return api; | ||
} | ||
|
||
public OAuthRequest createRequest(Verb verb, String endpoint) | ||
throws InterruptedException, ExecutionException, IOException { | ||
return new OAuthRequest(verb, api.getBaseAddress() + endpoint); | ||
} | ||
|
||
public Response executeRequest(OAuthRequest request) throws InterruptedException, ExecutionException, IOException { | ||
authenticationService.signRequest(accessToken, request); | ||
return authenticationService.execute(request); | ||
} | ||
|
||
public Response executeRequest(OAuthRequest request, int expectedStatusCode) | ||
throws InterruptedException, ExecutionException, IOException { | ||
Response response = executeRequest(request); | ||
if (response.getCode() != expectedStatusCode) { | ||
response.getBody(); | ||
throw new RuntimeException("Status code is not " + expectedStatusCode + ": " + response); | ||
} | ||
return response; | ||
} | ||
|
||
private Map<String, String> getConsumerToken() throws IOException { | ||
byte[] rawRequest = ("client=" + clientId).getBytes(StandardCharsets.UTF_8); | ||
HttpURLConnection connection = getConnection(api.getBaseAddress() + "/oauth1/consumer_token", "POST", true, | ||
true); | ||
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); | ||
connection.setRequestProperty("Content-Length", Integer.toString(rawRequest.length)); | ||
connection.connect(); | ||
connection.getOutputStream().write(rawRequest); | ||
connection.getOutputStream().flush(); | ||
String content = StreamUtils.getStreamContents(connection.getInputStream()); | ||
connection.disconnect(); | ||
|
||
return new Gson().fromJson(content, Map.class); | ||
} | ||
|
||
private static String authorize(String authorizationURL) throws IOException { | ||
HttpURLConnection connection = getConnection(authorizationURL, "GET", true, false); | ||
connection.connect(); | ||
String content = StreamUtils.getStreamContents(connection.getInputStream()); | ||
connection.disconnect(); | ||
return content.substring(content.indexOf('=') + 1); | ||
} | ||
|
||
private static HttpURLConnection getConnection(String rawURL, String method, boolean doInput, boolean doOutput) | ||
throws IOException { | ||
URL url = new URL(rawURL); | ||
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); | ||
connection.setDoInput(doInput); | ||
connection.setDoOutput(doOutput); | ||
connection.setRequestMethod(method); | ||
connection.setRequestProperty("Accept", "*"); | ||
connection.setInstanceFollowRedirects(false); | ||
connection.setRequestProperty("charset", "utf-8"); | ||
connection.setUseCaches(false); | ||
return connection; | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
src/eu/gruning/discofox/apiclient/DiscovergyApiEngine.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package eu.gruning.discofox.apiclient; | ||
|
||
import java.io.IOException; | ||
import java.lang.reflect.Type; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.concurrent.ExecutionException; | ||
|
||
import org.apache.logging.log4j.LogManager; | ||
import org.apache.logging.log4j.Logger; | ||
|
||
import com.github.scribejava.core.model.OAuthRequest; | ||
import com.github.scribejava.core.model.Verb; | ||
import com.google.gson.Gson; | ||
import com.google.gson.JsonSyntaxException; | ||
import com.google.gson.reflect.TypeToken; | ||
|
||
import eu.gruning.discofox.apiobjects.Meter; | ||
import eu.gruning.discofox.apiobjects.Reading; | ||
import eu.gruning.discofox.apiobjects.Readings; | ||
import eu.gruning.discofox.internal.Configuration; | ||
|
||
public class DiscovergyApiEngine { | ||
private final DiscovergyApi api; | ||
private final DiscovergyApiClient client; | ||
private final Type listMeterType = new TypeToken<List<Meter>>() { | ||
}.getType(); | ||
private static final Logger logger = LogManager.getLogger(DiscovergyApiEngine.class); | ||
|
||
public DiscovergyApiEngine(Configuration config) throws IOException, InterruptedException, ExecutionException { | ||
this.api = new DiscovergyApi(config.getUser(), config.getPassword()); | ||
this.client = new DiscovergyApiClient(api, config.getClientId()); | ||
logger.info("Sucessfully connected to Discovergy API at " + config.getaWATTarBaseURL() + " with user " + config.getUser()); | ||
} | ||
|
||
public List<Meter> getMeters() throws IOException, InterruptedException, ExecutionException { | ||
return new Gson().fromJson(client.executeRequest(client.createRequest(Verb.GET, "/meters"), 200).getBody(), listMeterType); | ||
} | ||
|
||
public Reading getLastReading(String meterId) throws IOException, InterruptedException, ExecutionException { | ||
OAuthRequest request = client.createRequest(Verb.GET, "/last_reading"); | ||
request.addQuerystringParameter("meterId", meterId); | ||
Reading reading; | ||
try { | ||
reading = new Gson().fromJson(client.executeRequest(request, 200).getBody(), Reading.class); | ||
reading.setMeterId(meterId); | ||
} catch (JsonSyntaxException e) { | ||
// in case of syntax exception return empty object | ||
reading = new Reading(); | ||
} | ||
return reading; | ||
} | ||
|
||
private Readings getReadingsInternal(String meterId, long start, long end) throws IOException, InterruptedException, ExecutionException { | ||
OAuthRequest request = client.createRequest(Verb.GET, "/readings"); | ||
request.addQuerystringParameter("meterId", meterId); | ||
request.addQuerystringParameter("resolution", "raw"); | ||
request.addQuerystringParameter("from", String.valueOf(start)); | ||
request.addQuerystringParameter("to", String.valueOf(end)); | ||
logger.debug("Getting readings for meter " + meterId + ", resolution: raw, from: " + start + " to " + end); | ||
String result = client.executeRequest(request, 200).getBody(); | ||
logger.debug("Result: " + result); | ||
Readings readings = new Readings(); | ||
try { | ||
Reading[] r = new Gson().fromJson(result, Reading[].class); | ||
readings.setReadings(Arrays.asList(r)); | ||
readings.setMeterId(meterId); | ||
} catch (JsonSyntaxException e) { | ||
logger.info("JSON conversion of readings failed: " + e.getMessage()); | ||
// in case of syntax exception return empty object | ||
} | ||
return readings; | ||
} | ||
|
||
public Readings getReadings(String meterId, long start, long end) throws IOException, InterruptedException, ExecutionException { | ||
// Handle situations where a day has 25 hours which can happen with switching to | ||
// daylight saving time | ||
// The Discovergy API will refuse any request with more than 24 hours between | ||
// start and end | ||
// In this special case we will first query 24 hours and then another time 1 | ||
// hour | ||
if (end - start <= 86400000) { | ||
return getReadingsInternal(meterId, start, end); | ||
} else { | ||
Readings readings = getReadingsInternal(meterId, start, start + 86399999); | ||
readings.append(getReadingsInternal(meterId, start + 86400000, end)); | ||
return readings; | ||
} | ||
} | ||
} |
Oops, something went wrong.