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. + * + *

+ */ + 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