Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/open dtu #6

Merged
merged 7 commits into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion io.openems.edge.application/EdgeApp.bndrun
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
bnd.identity;id='io.openems.edge.timeofusetariff.corrently',\
bnd.identity;id='io.openems.edge.timeofusetariff.entsoe',\
bnd.identity;id='io.openems.edge.timeofusetariff.tibber',\
bnd.identity;id='io.openems.edge.io.opendtu',\

-runbundles: \
Java-WebSocket;version='[1.5.4,1.5.5)',\
Expand Down Expand Up @@ -377,6 +378,7 @@
io.openems.wrapper.retrofit-converter-scalars;version=snapshot,\
io.openems.wrapper.retrofit2;version=snapshot,\
io.openems.wrapper.sdnotify;version=snapshot,\
io.openems.edge.io.opendtu;version=snapshot,\
io.reactivex.rxjava3.rxjava;version='[3.1.8,3.1.9)',\
javax.jmdns;version='[3.4.1,3.4.2)',\
javax.xml.soap-api;version='[1.4.0,1.4.1)',\
Expand Down Expand Up @@ -409,4 +411,4 @@
org.osgi.util.promise;version='[1.3.0,1.3.1)',\
org.owasp.encoder;version='[1.2.3,1.2.4)',\
reactive-streams;version='[1.0.4,1.0.5)',\
rrd4j;version='[3.9.0,3.9.1)'
rrd4j;version='[3.9.0,3.9.1)'
14 changes: 14 additions & 0 deletions io.openems.edge.io.opendtu/bnd.bnd
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Bundle-Name: OpenEMS Edge IO OpenDTU
Bundle-Vendor: Haller Johannes
Bundle-License: https://opensource.org/licenses/EPL-2.0
Bundle-Version: 1.0.0.${tstamp}

-buildpath: \
${buildpath},\
io.openems.common,\
io.openems.edge.common,\
io.openems.edge.io.api,\
io.openems.edge.meter.api,\
io.openems.edge.bridge.http
-testpath: \
${testpath}
8 changes: 8 additions & 0 deletions io.openems.edge.io.opendtu/readme.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
= openDTU Hoymiles

This bundle implements openDTU for Hoymiles Inverters.

Compatible with
-https://github.com/tbnobody/OpenDTU

https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.io.opendtu[Source Code icon:github[]]
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package io.openems.edge.io.opendtu.common;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Base64;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.exceptions.OpenemsException;
import io.openems.common.utils.JsonUtils;

/**
* Implements the local openDTU REST Api.
*
* <p>
* See https://github.com/tbnobody/OpenDTU
*/
public class OpendtuApi {

private final String baseUrl;
private final String username; // for basic auth
private final String password; // for basic auth


public OpendtuApi(String ip, String username, String password) {
this.baseUrl = "http://" + ip;
this.username = username;
this.password = password;
}

/**
* Gets the power limit status of all inverters.
*
* @return the power limit status as JsonObject
* @throws OpenemsNamedException on error
*/
public JsonObject getLimitStatus() throws OpenemsNamedException {
return JsonUtils.getAsJsonObject(this.sendRequest("GET", "/api/limit/status", null));
}

/**
* Sets the relative power limit of a specific inverter.
*
* @param serial the inverter's serial number
* @param limitType the type of limit
* @param limitValue the value of the limit
* @return the response as JsonObject
* @throws OpenemsNamedException on error
*/
public JsonObject setPowerLimit(String serial, int limitType, int limitValue) throws OpenemsNamedException {
JsonObject innerData = new JsonObject();
innerData.addProperty("serial", serial);
innerData.addProperty("limit_value", limitValue);
innerData.addProperty("limit_type", limitType);

// Convert innerData to a String, and then parse it back to a JsonObject
JsonObject payload = innerData;
return JsonUtils.getAsJsonObject(this.sendRequest("POST", "/api/limit/config", payload));
}



/**
* Gets the status of the device.
*
* <p>
* See https://github.com/tbnobody/OpenDTU
*
* @return the status as JsonObject according to Shelly docs
* @throws OpenemsNamedException on error
*/
public JsonObject getStatusForInverter(String serialNumber) throws OpenemsNamedException {
return JsonUtils.getAsJsonObject(this.sendGetRequest("/api/livedata/status?inv=" + serialNumber));
}


/**
* Sends a get request to the openDTU.
*
* @param endpoint the REST Api endpoint
* @return a JsonObject or JsonArray
* @throws OpenemsNamedException on error
*/
private JsonElement sendGetRequest(String endpoint) throws OpenemsNamedException {
try {
var url = new URL(this.baseUrl + endpoint);
var con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
con.setConnectTimeout(5000);
con.setReadTimeout(5000);
var status = con.getResponseCode();
String body;
try (var in = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
// Read HTTP response
var content = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
content.append(line);
content.append(System.lineSeparator());
}
body = content.toString();
}
if (status < 300) {
// Parse response to JSON
return JsonUtils.parseToJsonObject(body);
}
throw new OpenemsException("Error while reading from openDTU API. Response code: " + status + ". " + body);
} catch (OpenemsNamedException | IOException e) {
throw new OpenemsException(
"Unable to read from openDTU API. " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}




/**
* Writes a value to the openDTU.
*
* @param endpoint the REST Api endpoint
* @param data the JSON data to be written
* @throws OpenemsNamedException on error
*/
public void writeValue(String endpoint, JsonObject data) throws OpenemsNamedException {
this.sendRequest("POST", endpoint, data);
}

private JsonElement sendRequest(String method, String endpoint, JsonObject data) throws OpenemsNamedException {
try {
var url = new URL(this.baseUrl + endpoint);
var con = (HttpURLConnection) url.openConnection();
con.setRequestMethod(method);
con.setConnectTimeout(5000);
con.setReadTimeout(5000);

// Add Basic Authentication header
String auth = this.username + ":" + this.password;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
con.setRequestProperty("Authorization", "Basic " + encodedAuth);

if ("POST".equals(method) && data != null) {
con.setDoOutput(true);
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // Set the content type
String postData = "data=" + data.toString();

// Debug Log: Displaying the POST data being sent
// System.out.println("Sending POST request to URL: " + url.toString());
// System.out.println("POST Data: " + postData);

try (var out = new OutputStreamWriter(con.getOutputStream())) {
out.write(postData); // Write the formatted POST data
}
}

var status = con.getResponseCode();
String body;
try (var in = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
var content = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
content.append(line);
content.append(System.lineSeparator());
}
body = content.toString();
}

if (status < 300) {
return JsonUtils.parseToJsonObject(body);
}

throw new OpenemsException("Error with openDTU API. Response code: " + status + ". " + body);
} catch (OpenemsNamedException | IOException e) {
throw new OpenemsException(
"Unable to communicate with openDTU API. " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.openems.edge.io.opendtu.inverter;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

import io.openems.edge.meter.api.MeterType;
import io.openems.edge.meter.api.SinglePhase;

@ObjectClassDefinition(//
name = "openDTU Hoymiles Inverter", //
description = "Implements the openDTU for Hoymiles Inverter.")
@interface Config {

@AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component")
String id() default "io0";

@AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID")
String alias() default "";

@AttributeDefinition(name = "Username", description = "Username for openDTU to make settings possible")
String username() default "";

@AttributeDefinition(name = "Password", description = "Password for oprnDTU to make settings possible", type = AttributeType.PASSWORD)
String password() default "";

@AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?")
boolean enabled() default true;

@AttributeDefinition(name = "Phase", description = "Which Phase is this Inverter connected to?")
SinglePhase phase() default SinglePhase.L1;

@AttributeDefinition(name = "IP-Address", description = "The IP address of the openDTU.")
String ip();

@AttributeDefinition(name = "Inverter Serial Number", description = "The serial number of the inverter connected to the DTU")
String serialNumber() default "";

@AttributeDefinition(name = "Initial Power Limit", description = "The initial power limit setting")
int initialPowerLimit() default 100;

@AttributeDefinition(name = "Meter-Type", description = "What is measured by this DTU?")
MeterType type() default MeterType.PRODUCTION;

String webconsole_configurationFactory_nameHint() default "IO openDTU Device [{id}]";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package io.openems.edge.io.opendtu.inverter;

import org.osgi.service.event.EventHandler;

import io.openems.common.channel.AccessMode;
import io.openems.common.channel.Level;
import io.openems.common.channel.PersistencePriority;
import io.openems.common.channel.Unit;
import io.openems.common.types.OpenemsType;

import io.openems.edge.common.channel.Doc;

import io.openems.edge.common.channel.StateChannel;
import io.openems.edge.common.channel.WriteChannel;
import io.openems.edge.common.channel.value.Value;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.meter.api.ElectricityMeter;
import io.openems.edge.meter.api.SinglePhaseMeter;

public interface Opendtu
extends SinglePhaseMeter, ElectricityMeter, OpenemsComponent, EventHandler {



/**
* Channel for setting the Power Limit.
*/
public default WriteChannel<Integer> setPowerLimit() {
return this.channel(ChannelId.SET_POWER_LIMIT);
}



public static enum ChannelId implements io.openems.edge.common.channel.ChannelId {
/**
* Slave Communication Failed Fault.
*
* <ul>
* <li>Interface: Opendtu
* <li>Type: State
* </ul>
*/
/**
* Maximum Ever Actual Power.
*
* <ul>
* <li>Interface: Ess DC Charger
* <li>Type: Integer
* <li>Unit: W
* <li>Range: positive or '0'
* <li>Implementation Note: value is automatically derived from ACTUAL_POWER
* </ul>
*/
MAX_ACTUAL_POWER(Doc.of(OpenemsType.INTEGER)//
.unit(Unit.WATT) //
.persistencePriority(PersistencePriority.HIGH)), //

WARN_INVERTER_NOT_REACHABLE(Doc.of(Level.INFO) //
.text("One or more Inverter is not reachable")),
SLAVE_COMMUNICATION_FAILED(Doc.of(Level.FAULT)),
LIMIT_STATUS(Doc.of(OpenemsType.STRING).text("Limit Status")),
SET_POWER_LIMIT(Doc.of(OpenemsType.INTEGER).text("Set Power Limit Status").accessMode(AccessMode.READ_WRITE));




private final Doc doc;

private ChannelId(Doc doc) {
this.doc = doc;
}

@Override
public Doc doc() {
return this.doc;
}
}


/**
* Gets the Channel for {@link ChannelId#SLAVE_COMMUNICATION_FAILED}.
*
* @return the Channel
*/
public default StateChannel getSlaveCommunicationFailedChannel() {
return this.channel(ChannelId.SLAVE_COMMUNICATION_FAILED);
}

/**
* Gets the Slave Communication Failed State. See
* {@link ChannelId#SLAVE_COMMUNICATION_FAILED}.
*
* @return the Channel {@link Value}
*/
public default Value<Boolean> getSlaveCommunicationFailed() {
return this.getSlaveCommunicationFailedChannel().value();
}

/**
* Internal method to set the 'nextValue' on
* {@link ChannelId#SLAVE_COMMUNICATION_FAILED} Channel.
*
* @param value the next value
*/
public default void _setSlaveCommunicationFailed(boolean value) {
this.getSlaveCommunicationFailedChannel().setNextValue(value);
}
}
Loading