diff --git a/io.openems.edge.meter.opendtu/.classpath b/io.openems.edge.meter.opendtu/.classpath
new file mode 100644
index 0000000000..b4cffd0fe6
--- /dev/null
+++ b/io.openems.edge.meter.opendtu/.classpath
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/io.openems.edge.meter.opendtu/.project b/io.openems.edge.meter.opendtu/.project
new file mode 100644
index 0000000000..eb2d90e6fa
--- /dev/null
+++ b/io.openems.edge.meter.opendtu/.project
@@ -0,0 +1,23 @@
+
+
+ io.openems.edge.meter.opendtu
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ bndtools.core.bndbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ bndtools.core.bndnature
+
+
diff --git a/io.openems.edge.meter.opendtu/bnd.bnd b/io.openems.edge.meter.opendtu/bnd.bnd
new file mode 100644
index 0000000000..059511dafd
--- /dev/null
+++ b/io.openems.edge.meter.opendtu/bnd.bnd
@@ -0,0 +1,15 @@
+Bundle-Name: OpenEMS Edge Meter OpenDTU
+Bundle-Vendor:
+Bundle-License: https://opensource.org/licenses/EPL-2.0
+Bundle-Version: 1.0.0.${tstamp}
+
+-buildpath: \
+ ${buildpath},\
+ io.openems.common,\
+ io.openems.edge.bridge.http,\
+ io.openems.edge.common,\
+ io.openems.edge.meter.api,\
+ io.openems.edge.timedata.api,\
+ io.openems.common.bridge.http
+-testpath: \
+ ${testpath}
diff --git a/io.openems.edge.meter.opendtu/readme.adoc b/io.openems.edge.meter.opendtu/readme.adoc
new file mode 100644
index 0000000000..9e61d8bc34
--- /dev/null
+++ b/io.openems.edge.meter.opendtu/readme.adoc
@@ -0,0 +1,3 @@
+= OpenDTU Meter
+
+This bundle implements requesting power, voltage and current from an OpenDTU.
diff --git a/io.openems.edge.meter.opendtu/src/io/openems/edge/meter/opendtu/Config.java b/io.openems.edge.meter.opendtu/src/io/openems/edge/meter/opendtu/Config.java
new file mode 100644
index 0000000000..cae6e1ed2e
--- /dev/null
+++ b/io.openems.edge.meter.opendtu/src/io/openems/edge/meter/opendtu/Config.java
@@ -0,0 +1,33 @@
+package io.openems.edge.meter.opendtu;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+import io.openems.common.types.MeterType;
+import io.openems.edge.common.type.Phase.SinglePhase;
+
+@ObjectClassDefinition(name = "Meter OpenDTU", //
+ description = "Implements the metering component for OpenDTU via HTTP API")
+@interface Config {
+
+ @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component")
+ String id() default "meterOpenDTU0";
+
+ @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID")
+ String alias() default "";
+
+ @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?")
+ boolean enabled() default true;
+
+ @AttributeDefinition(name = "Phase", description = "Which Phase is the Inverter connected to?")
+ SinglePhase phase() default SinglePhase.L1;
+
+ @AttributeDefinition(name = "IP-Address", description = "The IP address of the OpenDTU.")
+ String ipAddress();
+
+ @AttributeDefinition(name = "Inverter Serial Number", description = "Serial Number of the Inverter")
+ String serialNumber() default "";
+
+ String webconsole_configurationFactory_nameHint() default "Meter OpenDTU[{id}]";
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.meter.opendtu/src/io/openems/edge/meter/opendtu/MeterOpenDtu.java b/io.openems.edge.meter.opendtu/src/io/openems/edge/meter/opendtu/MeterOpenDtu.java
new file mode 100644
index 0000000000..6aa0e69dd8
--- /dev/null
+++ b/io.openems.edge.meter.opendtu/src/io/openems/edge/meter/opendtu/MeterOpenDtu.java
@@ -0,0 +1,72 @@
+package io.openems.edge.meter.opendtu;
+
+import io.openems.common.channel.Level;
+import io.openems.edge.common.channel.Doc;
+import io.openems.edge.common.channel.StateChannel;
+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 MeterOpenDtu extends ElectricityMeter, SinglePhaseMeter, OpenemsComponent {
+
+ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
+
+ /**
+ * Slave Communication Failed Fault.
+ *
+ *
+ * Indicates a failure in communication with a slave device, which might affect
+ * system operations.
+ *
+ *
+ * - Interface: MeterOpenDtu
+ *
- Type: State
+ *
+ */
+ SLAVE_COMMUNICATION_FAILED(Doc.of(Level.FAULT) //
+ .text("Communication with slave device failed."));
+
+ 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 StateChannel representing communication failure with a slave
+ * device.
+ */
+ public default StateChannel getSlaveCommunicationFailedChannel() {
+ return this.channel(ChannelId.SLAVE_COMMUNICATION_FAILED);
+ }
+
+ /**
+ * Gets the current state of the Slave Communication Failed channel.
+ *
+ * @return the Channel {@link Value} indicating whether communication has
+ * failed.
+ */
+ public default Value getSlaveCommunicationFailed() {
+ return this.getSlaveCommunicationFailedChannel().value();
+ }
+
+ /**
+ * Internal method to set the 'nextValue' on
+ * {@link ChannelId#SLAVE_COMMUNICATION_FAILED} Channel.
+ *
+ * @param value the next value indicating communication failure state.
+ */
+ public default void _setSlaveCommunicationFailed(boolean value) {
+ this.getSlaveCommunicationFailedChannel().setNextValue(value);
+ }
+
+}
diff --git a/io.openems.edge.meter.opendtu/src/io/openems/edge/meter/opendtu/MeterOpenDtuImpl.java b/io.openems.edge.meter.opendtu/src/io/openems/edge/meter/opendtu/MeterOpenDtuImpl.java
new file mode 100644
index 0000000000..a2eb1d9145
--- /dev/null
+++ b/io.openems.edge.meter.opendtu/src/io/openems/edge/meter/opendtu/MeterOpenDtuImpl.java
@@ -0,0 +1,221 @@
+package io.openems.edge.meter.opendtu;
+
+import static io.openems.common.utils.JsonUtils.getAsJsonObject;
+import static io.openems.edge.common.channel.ChannelUtils.setValue;
+import static org.osgi.service.component.annotations.ReferenceCardinality.MANDATORY;
+
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
+import org.osgi.service.event.propertytypes.EventTopics;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonElement;
+
+import io.openems.common.channel.AccessMode;
+import io.openems.common.exceptions.InvalidValueException;
+import io.openems.common.exceptions.OpenemsException;
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.common.types.MeterType;
+import io.openems.common.utils.JsonUtils;
+import io.openems.edge.bridge.http.cycle.HttpBridgeCycleServiceDefinition;
+
+import io.openems.common.bridge.http.api.BridgeHttp;
+import io.openems.common.bridge.http.api.BridgeHttpFactory;
+import io.openems.common.bridge.http.api.HttpResponse;
+
+import io.openems.edge.common.component.AbstractOpenemsComponent;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.common.event.EdgeEventConstants;
+import io.openems.edge.common.modbusslave.ModbusSlave;
+import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable;
+import io.openems.edge.common.modbusslave.ModbusSlaveTable;
+import io.openems.edge.common.type.Phase.SinglePhase;
+import io.openems.edge.meter.api.ElectricityMeter;
+import io.openems.edge.meter.api.SinglePhaseMeter;
+import io.openems.edge.timedata.api.Timedata;
+import io.openems.edge.timedata.api.TimedataProvider;
+import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower;
+
+import static io.openems.common.utils.JsonUtils.getAsFloat;
+import static java.lang.Math.round;
+
+@Designate(ocd = Config.class, factory = true)
+@Component(//
+ name = "Meter.OpenDTU", //
+ immediate = true, //
+ configurationPolicy = ConfigurationPolicy.REQUIRE //
+)
+@EventTopics({ //
+ EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE, //
+ EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE //
+})
+public class MeterOpenDtuImpl extends AbstractOpenemsComponent
+ implements MeterOpenDtu, ElectricityMeter, SinglePhaseMeter, OpenemsComponent, TimedataProvider, EventHandler, ModbusSlave {
+
+ private final CalculateEnergyFromPower calculateProductionEnergy = new CalculateEnergyFromPower(this,
+ ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY);
+ private final CalculateEnergyFromPower calculateConsumptionEnergy = new CalculateEnergyFromPower(this,
+ ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY);
+
+
+ private final Logger log = LoggerFactory.getLogger(MeterOpenDtuImpl.class);
+
+ private String baseUrl;
+ private Config config;
+
+ @Reference(cardinality = MANDATORY)
+ private BridgeHttpFactory httpBridgeFactory;
+ private BridgeHttp httpBridge;
+ @Reference
+ private HttpBridgeCycleServiceDefinition httpBridgeCycleServiceDefinition;
+
+ @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL)
+ private volatile Timedata timedata;
+
+ public MeterOpenDtuImpl() {
+ super(//
+ OpenemsComponent.ChannelId.values(), //
+ ElectricityMeter.ChannelId.values(), //
+ MeterOpenDtu.ChannelId.values() //
+ );
+
+ SinglePhaseMeter.calculateSinglePhaseFromActivePower(this);
+ SinglePhaseMeter.calculateSinglePhaseFromCurrent(this);
+ SinglePhaseMeter.calculateSinglePhaseFromVoltage(this);
+
+ }
+
+ @Activate
+ protected void activate(ComponentContext context, Config config) throws InvalidValueException, KeyManagementException, NoSuchAlgorithmException, OpenemsException {
+ super.activate(context, config.id(), config.alias(), config.enabled());
+ this.config = config;
+
+ this.baseUrl = "http://" + config.ipAddress();
+ this.httpBridge = this.httpBridgeFactory.get();
+
+ if (!this.isEnabled()) {
+ return;
+ }
+
+ final var cycleService = this.httpBridge.createService(this.httpBridgeCycleServiceDefinition);
+ cycleService.subscribeJsonEveryCycle(this.baseUrl + "/api/livedata/status?inv=" + config.serialNumber(), this::processHttpResult);
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ if (this.httpBridge != null) {
+ this.httpBridgeFactory.unget(this.httpBridge);
+ this.httpBridge = null;
+ }
+ super.deactivate();
+ }
+
+
+ @Override
+ public String debugLog() {
+ return this.getPhase() + ":" + this.getActivePower().asString();
+ }
+
+ @Override
+ public void handleEvent(Event event) {
+ if (!this.isEnabled()) {
+ return;
+ }
+
+ switch (event.getTopic()) {
+ case EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE: //
+ this.calculateEnergy();
+ break;
+ }
+ }
+
+ private void processHttpResult(HttpResponse result, Throwable error) {
+ setValue(this, MeterOpenDtu.ChannelId.SLAVE_COMMUNICATION_FAILED, result == null);
+
+ Integer power = null;
+ Integer voltage = null;
+ Integer current = null;
+
+ if (error != null) {
+ this.logDebug(this.log, error.getMessage());
+
+ } else {
+ try {
+ var jsonResponse = getAsJsonObject(result.data());
+ var inverters = JsonUtils.getAsJsonArray(jsonResponse, "inverters");
+ var inverter = inverters.get(0).getAsJsonObject();
+ var ac = inverter.getAsJsonObject("AC");
+ var ac0 = ac.getAsJsonObject("0");
+
+ power = round(getAsFloat(ac0.getAsJsonObject("Power"), "v"));
+ voltage = round(getAsFloat(ac0.getAsJsonObject("Voltage"), "v") * 1000);
+ current = round(getAsFloat(ac0.getAsJsonObject("Current"), "v") * 1000);
+
+ } catch (OpenemsNamedException e) {
+ this.logDebug(this.log, e.getMessage());
+ }
+ }
+
+ this._setActivePower(power);
+ this._setCurrent(current);
+ this._setVoltage(voltage);
+
+ }
+
+ /**
+ * Calculate the Energy values from ActivePower.
+ */
+ private void calculateEnergy() {
+ // Calculate Energy
+ final var activePower = this.getActivePower().get();
+ if (activePower == null) {
+ this.calculateProductionEnergy.update(null);
+ this.calculateConsumptionEnergy.update(null);
+ } else if (activePower >= 0) {
+ this.calculateProductionEnergy.update(activePower);
+ this.calculateConsumptionEnergy.update(0);
+ } else {
+ this.calculateProductionEnergy.update(0);
+ this.calculateConsumptionEnergy.update(-activePower);
+ }
+ }
+
+ @Override
+ public Timedata getTimedata() {
+ return this.timedata;
+ }
+
+ @Override
+ public MeterType getMeterType() {
+ return MeterType.PRODUCTION;
+ }
+
+ @Override
+ public SinglePhase getPhase() {
+ return this.config.phase();
+ }
+
+ @Override
+ public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) {
+ return new ModbusSlaveTable(//
+ OpenemsComponent.getModbusSlaveNatureTable(accessMode), //
+ ElectricityMeter.getModbusSlaveNatureTable(accessMode), //
+ ModbusSlaveNatureTable.of(MeterOpenDtu.class, accessMode, 100) //
+ .build());
+ }
+
+}
diff --git a/io.openems.edge.meter.opendtu/test/io/openems/edge/meter/opendtu/MeterOpenDtuImplTest.java b/io.openems.edge.meter.opendtu/test/io/openems/edge/meter/opendtu/MeterOpenDtuImplTest.java
new file mode 100644
index 0000000000..5df3d7f6d8
--- /dev/null
+++ b/io.openems.edge.meter.opendtu/test/io/openems/edge/meter/opendtu/MeterOpenDtuImplTest.java
@@ -0,0 +1,91 @@
+package io.openems.edge.meter.opendtu;
+
+import static io.openems.common.types.MeterType.PRODUCTION;
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import io.openems.edge.common.test.AbstractComponentTest.TestCase;
+import io.openems.common.bridge.http.api.HttpResponse;
+import io.openems.edge.bridge.http.cycle.HttpBridgeCycleServiceDefinition;
+import io.openems.edge.bridge.http.cycle.dummy.DummyCycleSubscriber;
+import io.openems.edge.common.test.ComponentTest;
+import io.openems.edge.common.type.Phase.SinglePhase;
+import io.openems.edge.meter.api.ElectricityMeter;
+import io.openems.edge.timedata.test.DummyTimedata;
+import io.openems.common.bridge.http.dummy.DummyBridgeHttpBundle;
+
+
+public class MeterOpenDtuImplTest {
+
+ @Test
+ public void test() throws Exception {
+ final var odtu = new MeterOpenDtuImpl();
+ final var httpTestBundle = new DummyBridgeHttpBundle();
+ final var dummyCycleSubscriber = new DummyCycleSubscriber();
+ new ComponentTest(odtu) //
+ .addReference("httpBridgeFactory", httpTestBundle.factory()) //
+ .addReference("httpBridgeCycleServiceDefinition",
+ new HttpBridgeCycleServiceDefinition(dummyCycleSubscriber)) //
+ .addReference("timedata", new DummyTimedata("timedata0")) //
+ .activate(MyConfig.create() //
+ .setId("meterOpenDTU0") //
+ .setIp("127.0.0.1") //
+ .setPhase(SinglePhase.L1) //
+ .setType(PRODUCTION) //
+ .setSerialNumber("1234567890")
+
+ .build()) //
+ .next(new TestCase("Successful read response") //
+ .onBeforeProcessImage(() -> {
+ httpTestBundle.forceNextSuccessfulResult(HttpResponse.ok("""
+ {
+ "inverters":[
+ {
+ "AC":{
+ "0":{
+ "Power":{
+ "v":123,
+ "u":"W",
+ "d":1
+ },
+ "Voltage":{
+ "v":228.2,
+ "u":"V",
+ "d":1
+ },
+ "Current":{
+ "v":1,
+ "u":"A",
+ "d":2
+ },
+ "Frequency":{
+ "v":49.98,
+ "u":"Hz",
+ "d":2
+ },
+ "PowerFactor":{
+ "v":0,
+ "u":"",
+ "d":3
+ },
+ "ReactivePower":{
+ "v":0,
+ "u":"var",
+ "d":1
+ }
+ }
+ }
+ }
+ ]
+ }
+ """));
+ dummyCycleSubscriber.triggerNextCycle();
+ }) //
+ .onAfterProcessImage(() -> assertEquals("L1:123 W", odtu.debugLog()))
+ .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 123) //
+ .output(ElectricityMeter.ChannelId.VOLTAGE, 228200) //
+ .output(ElectricityMeter.ChannelId.CURRENT, 1000) //
+)
+ .deactivate();
+ }
+}
diff --git a/io.openems.edge.meter.opendtu/test/io/openems/edge/meter/opendtu/MyConfig.java b/io.openems.edge.meter.opendtu/test/io/openems/edge/meter/opendtu/MyConfig.java
new file mode 100644
index 0000000000..21203acc6a
--- /dev/null
+++ b/io.openems.edge.meter.opendtu/test/io/openems/edge/meter/opendtu/MyConfig.java
@@ -0,0 +1,81 @@
+package io.openems.edge.meter.opendtu;
+
+import io.openems.common.test.AbstractComponentConfig;
+import io.openems.common.types.MeterType;
+import io.openems.edge.common.type.Phase.SinglePhase;
+
+@SuppressWarnings("all")
+public class MyConfig extends AbstractComponentConfig implements Config {
+
+ protected static class Builder {
+ private String id;
+ private String ipAddress;
+ private String serialNumber;
+ private MeterType type;
+ private SinglePhase phase;
+
+ private Builder() {
+ }
+
+ public Builder setId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder setIp(String ip) {
+ this.ipAddress = ip;
+ return this;
+ }
+
+ public Builder setSerialNumber(String serialNumber) {
+ this.serialNumber = serialNumber;
+ return this;
+ }
+
+ public Builder setType(MeterType type) {
+ this.type = type;
+ return this;
+ }
+
+ public Builder setPhase(SinglePhase phase) {
+ this.phase = phase;
+ return this;
+ }
+
+ public MyConfig build() {
+ return new MyConfig(this);
+ }
+ }
+
+ /**
+ * Create a Config builder.
+ *
+ * @return a {@link Builder}
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ private final Builder builder;
+
+ private MyConfig(Builder builder) {
+ super(Config.class, builder.id);
+ this.builder = builder;
+ }
+
+ @Override
+ public String ipAddress() {
+ return this.builder.ipAddress;
+ }
+
+ @Override
+ public SinglePhase phase() {
+ return this.builder.phase;
+ }
+
+ @Override
+ public String serialNumber() {
+ return this.builder.serialNumber;
+
+ }
+}
\ No newline at end of file