Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
Initial Release on Github
  • Loading branch information
martingruening authored Sep 10, 2020
1 parent e6e07b3 commit 9cb8d0b
Show file tree
Hide file tree
Showing 19 changed files with 1,508 additions and 0 deletions.
7 changes: 7 additions & 0 deletions configuration.json
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
}
71 changes: 71 additions & 0 deletions pom.xml
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>
62 changes: 62 additions & 0 deletions src/eu/gruning/discofox/apiclient/DiscovergyApi.java
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 src/eu/gruning/discofox/apiclient/DiscovergyApiClient.java
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 src/eu/gruning/discofox/apiclient/DiscovergyApiEngine.java
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;
}
}
}
Loading

0 comments on commit 9cb8d0b

Please sign in to comment.