Skip to content
Open
Show file tree
Hide file tree
Changes from 15 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
12 changes: 12 additions & 0 deletions io.openems.edge.meter.opendtu/.classpath
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="aQute.bnd.classpath.container"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21"/>
<classpathentry kind="src" output="bin" path="src"/>
<classpathentry kind="src" output="bin_test" path="test">
<attributes>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="bin"/>
</classpath>
23 changes: 23 additions & 0 deletions io.openems.edge.meter.opendtu/.project
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>io.openems.edge.meter.opendtu</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>bndtools.core.bndbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>bndtools.core.bndnature</nature>
</natures>
</projectDescription>
15 changes: 15 additions & 0 deletions io.openems.edge.meter.opendtu/bnd.bnd
Original file line number Diff line number Diff line change
@@ -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}
3 changes: 3 additions & 0 deletions io.openems.edge.meter.opendtu/readme.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
= OpenDTU Meter

This bundle implements requesting power, voltage and current from an OpenDTU.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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 measured by this Meter?")
SinglePhase phase() default SinglePhase.L1;

@AttributeDefinition(name = "Meter-Type", description = "Grid, Production (=default), Consumption")
MeterType type() default MeterType.PRODUCTION;

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

@AttributeDefinition(name = "Inverter Serial Number", description = "Serial Number of the Inverter")
String serialNumber() default "";

String webconsole_configurationFactory_nameHint() default "Meter OpenDTU[{id}]";

}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* Indicates a failure in communication with a slave device, which might affect
* system operations.
*
* <ul>
* <li>Interface: MeterOpenDtu
* <li>Type: State
* </ul>
*/
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<Boolean> 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);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
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<JsonElement> result, Throwable error) {
setValue(this, MeterOpenDtu.ChannelId.SLAVE_COMMUNICATION_FAILED, result == null);

Integer power = null;
Integer voltage = null;
Integer current = null;
//boolean restartRequired = false;

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 this.config.type();
}

@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());
}

}
Loading
Loading