Skip to content

Commit

Permalink
Introduce ElectricityMeter as replacement for SymmetricMeter & Asymme…
Browse files Browse the repository at this point in the history
…tricMeter (#2144)

Distinguishing between `SymmetricMeter` and `AsymmetricMeter` and having `AsymmetricMeter extends SymmetricMeter` did never really make sense physically. In fact the _asymmetric_ meter (i.e. the three-phase meter) is the _most normal_ case. Special cases are:
- a _symmetric_ meter, i.e. one that has the same values on all phases; this can be handled entirely by deriving the individual phase values from the sum values, via dividing the power by three.
- a _single-phase_ meter, i.e. one that only has values on one phase. This requires active configuration and then allows setting the other phases to zero. The `SinglePhaseMeter` Nature is kept, because it allows a use-case when UI should really only show one value for these devices.

The special cases can be represented properly by this newly introduced `ElectricityMeter`, which combines most of the Channels and replaces SymmetricMeter and AsymmetricMeter entirely. It also clearly distinguishes _electricity meters_ from _heat meters_ that are slowly coming to OpenEMS.

This PR includes:
- Replace SymmetricMeter and AsymmetricMeter
- Breaking:
  - Renamed Meter.Virtual.Asymmetric.Add -> Meter.Virtual.Add
  - Dropped Meter.Virtual.Symmetric.Add
  - Renamed Meter.Virtual.Symmetric.Subtract -> Meter.Virtual.Subtract
- Simulator Grid Meter Reacting: add Voltage and Current sum and per phase for better testing
- UI fixes
  - show negative current per phase
  - fix translation issues

It also tries to clarify the sometimes confusing representation of positive and negative values for Power and Current. See the Javadoc in ElectricityMeter and the discussion at https://community.openems.io/t/change-energy-channel-assignment-based-on-meter-type/1603/6.

As this PR touches a lot of files, it is kept as small, simple and non-breaking as possible. Follow-up PRs will add more features,
  • Loading branch information
sfeilmeier authored Jun 11, 2023
1 parent 0d6e3f2 commit 5e7d18c
Show file tree
Hide file tree
Showing 238 changed files with 3,366 additions and 4,039 deletions.
2 changes: 1 addition & 1 deletion doc/modules/ROOT/pages/coreconcepts.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ To group these similar devices and services, OpenEMS defines "Natures" as sets o
That is, a Nature extends a normal Java interface with channels.

Examples of abstracting physical devices using Natures are:
- "SymmetricMeter" for power meters
- "ElectricityMeter" for electricity meters
- "SymmetricEss" for symmetric battery energy storage systems
- "Evcs" for electric vehicle charging stations.

Expand Down
32 changes: 16 additions & 16 deletions doc/modules/ROOT/pages/edge/implement.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -202,16 +202,16 @@ Afterwards adjust the following content in the template `MeterSimulatedImpl.Java
configurationPolicy = ConfigurationPolicy.REQUIRE //
)
----
. Make the class implement the `SymmetricMeter` nature:
. Make the class implement the `ElectricityMeter` nature:
+
[source,java]
----
public class MeterSimulatedImpl extends AbstractOpenemsModbusComponent implements MeterSimulated, SymmetricMeter, OpenemsComponent, ModbusComponent {
public class MeterSimulatedImpl extends AbstractOpenemsModbusComponent implements MeterSimulated, ElectricityMeter, OpenemsComponent, ModbusComponent {
----
. Eclipse will underline `SymmetricMeter` and show you the error *SymmetricMeter cannot be resolved to a type*. Resolve it by importing adding an `import io.openems.edge.meter.api.SymmetricMeter;`.
. Eclipse will underline `ElectricityMeter` and show the error *ElectricityMeter cannot be resolved to a type*. Resolve it by adding the line `import io.openems.edge.meter.api.ElectricityMeter;` to the import section of the file.
+
NOTE: The easiest way to fix these kind of import errors is to to selecto btn:[Source] → btn:[Organize Imports] in the menu or simply press btn:[Ctrl] + btn:[Shift] + btn:[o]. Alternatively click the 'error light bulb' next to the line with the error and select btn:[Import 'SymmetricMeter' (io.openems.edge.meter.api)].
. Eclipse still complains and now underlines the class name `MeterSimulated` with the error *The type MeterSimulated must implement the inherited abstract method SymmetricMeter.getMeterType()*. Resolve it by adding an implementation of the `getMeterType()` method:
NOTE: The easiest way to fix these kind of import errors is to select btn:[Source] → btn:[Organize Imports] in the menu or simply press btn:[Ctrl] + btn:[Shift] + btn:[o]. Alternatively click the 'light bulb' next to the line with the error and select btn:[Import 'ElectricityMeter' (io.openems.edge.meter.api)].
. Eclipse still complains and now underlines the class name `MeterSimulated` with the error *The type MeterSimulated must implement the inherited abstract method ElectricityMeter.getMeterType()*. Resolve it by adding an implementation of the `getMeterType()` method:
+
[source,java]
----
Expand All @@ -220,14 +220,14 @@ public MeterType getMeterType() {
return this.config.type();
}
----
. Tell the OpenEMS framework that `MeterSimulated` provides the SymmetricMeter *Channels*, by adjusting the constructor:
. Tell the OpenEMS framework that `MeterSimulated` provides the ElectricityMeter *Channels*, by adjusting the constructor:
+
[source,java]
----
public MeterSimulatedImpl() {
super(//
OpenemsComponent.ChannelId.values(), //
SymmetricMeter.ChannelId.values(), //
ElectricityMeter.ChannelId.values(), //
ModbusComponent.ChannelId.values(), //
MeterSimulated.ChannelId.values() //
);
Expand All @@ -252,7 +252,7 @@ with
protected ModbusProtocol defineModbusProtocol() throws OpenemsException {
return new ModbusProtocol(this, //
new FC3ReadRegistersTask(1000, Priority.HIGH,
m(SymmetricMeter.ChannelId.ACTIVE_POWER, new SignedWordElement(1000))));
m(ElectricityMeter.ChannelId.ACTIVE_POWER, new SignedWordElement(1000))));
}
----
+
Expand Down Expand Up @@ -293,8 +293,8 @@ import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask;
import io.openems.edge.common.channel.Doc;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.taskmanager.Priority;
import io.openems.edge.meter.api.ElectricityMeter;
import io.openems.edge.meter.api.MeterType;
import io.openems.edge.meter.api.SymmetricMeter;
@Designate(ocd = Config.class, factory = true) // <1>
@Component(// <2>
Expand All @@ -303,14 +303,14 @@ import io.openems.edge.meter.api.SymmetricMeter;
configurationPolicy = ConfigurationPolicy.REQUIRE // <5>
)
public class MeterSimulatedImpl extends AbstractOpenemsModbusComponent // <6>
implements MeterSimulated, SymmetricMeter, OpenemsComponent, ModbusComponent { // <7>
implements MeterSimulated, ElectricityMeter, OpenemsComponent, ModbusComponent { // <7>
private Config config = null;
public MeterSimulatedImpl() {
super(// <8>
OpenemsComponent.ChannelId.values(), //
SymmetricMeter.ChannelId.values(), //
ElectricityMeter.ChannelId.values(), //
ModbusComponent.ChannelId.values(), //
MeterSimulated.ChannelId.values() //
);
Expand Down Expand Up @@ -341,7 +341,7 @@ public class MeterSimulatedImpl extends AbstractOpenemsModbusComponent // <6>
protected ModbusProtocol defineModbusProtocol() throws OpenemsException { // <13>
return new ModbusProtocol(this, // <14>
new FC3ReadRegistersTask(1000, Priority.HIGH, // <15>
m(SymmetricMeter.ChannelId.ACTIVE_POWER, new SignedWordElement(1000)))); // <16>
m(ElectricityMeter.ChannelId.ACTIVE_POWER, new SignedWordElement(1000)))); // <16>
}
@Override
Expand All @@ -364,7 +364,7 @@ public class MeterSimulatedImpl extends AbstractOpenemsModbusComponent // <6>
+
NOTE: If the device was using another protocol, it is advisable to use the *AbstractOpenemsComponent* class as a convenience layer instead of implementing everything required by the *OpenemsComponent* interface manually.
<7> The class implements *OpenemsComponent*. This makes it an xref:coreconcepts.adoc#_openems_component[OpenEMS Component].
The Device that we are is a *SymmetricMeter*. We already defined the required Channels in the _initializeChannels()_ method. Additionally the Component also needs to implement the Nature interface.
The Device that we are implementing is an *ElectricityMeter*. We already defined the required Channels in the _initializeChannels()_ method. Additionally the Component also needs to implement the Nature interface.
+
NOTE: In plain Java it is not required to add `implements OpenemsComponent` if we inherit from 'AbstractOpenemsComponent' or 'AbstractOpenemsModbusComponent'. Be aware that for OSGi dependency injection to function properly, it is still required to mention all implemented interfaces again, as it is not considering the complete inheritance tree.
+
Expand All @@ -374,7 +374,7 @@ NOTE: In plain Java it is not required to add `implements OpenemsComponent` if w
- This enum is empty, as we do not have custom Channels here.
- ChannelId enums require a Doc object that provides meta information about the Channel - e.g. the above ACTIVE_POWER Channel is defined as `ACTIVE_POWER(new Doc().type(OpenemsType.INTEGER).unit(Unit.WATT)`
====
<8> We call the constructor of the super class (`AbstractOpenemsModbusComponent`/`AbstractOpenemsComponent`) to initialize the Channels of the Component. It is important to list all ChannelId-Enums of all implemented Natures. The call takes the *ChannelId* declarations and creates a Channel instance for each of them; e.g. for the `SymmetricMeter.ACTIVE_POWER` ChannelId, an object instance of `IntegerReadChannel` is created that represents the Channel.
<8> We call the constructor of the super class (`AbstractOpenemsModbusComponent`/`AbstractOpenemsComponent`) to initialize the Channels of the Component. It is important to list all ChannelId-Enums of all implemented Natures. The call takes the *ChannelId* declarations and creates a Channel instance for each of them; e.g. for the `ElectricityMeter.ACTIVE_POWER` ChannelId, an object instance of `IntegerReadChannel` is created that represents the Channel.
<9> The `super.activate()` method requires an instance of *ConfigurationAdmin* as a parameter. Using the *@Reference* annotation the OSGi framework is going to provide the ConfigurationAdmin service via dependency injection.
<10> The Component utilizes an external Modbus Component (the _Modbus Bridge_) for the actual Modbus communication. We receive an instance of this service via dependency injection (like we did already for the _ConfigurationAdmin_ service). Most of the magic is handled by the _AbstractOpenemsModbusComponent_ implementation, but the way the OSGi framework works, we need to define the _@Reference_ explicitly here in the actual implementation of the component and call the parent `setModbus()` method.
<11> The *activate()* method (marked by the *@Activate* annotation) is called on activation of an object instance of this Component. It comes with a ComponentContext and an instance of a configuration in the form of a Config object. All logic for activating and deactivating the OpenEMS Component is hidden in the super classes and just needs to be called from here.
Expand All @@ -384,7 +384,7 @@ NOTE: In plain Java it is not required to add `implements OpenemsComponent` if w
<15> *FC3ReadRegistersTask* is an implementation of Modbus http://www.simplymodbus.ca/FC03.htm[function code 3 "Read Holding Registers" icon:external-link[]]. Its first parameter is the start address of the register block. The second parameter is a priority information that defines how often this register block needs to be queried. Following parameters are an arbitrary number of *ModbusElements*.
+
NOTE: Most Modbus function codes are available by their respective _FC*_ implementation classes.
<16> Here the internal *m()* method is used to make a simple 1-to-1 mapping between the Modbus element at address `1000` and the Channel _SymmetricMeter.ChannelId.ACTIVE_POWER_. The Modbus element is defined as a 16 bit word element with an signed integer value.
<16> Here the internal *m()* method is used to make a simple 1-to-1 mapping between the Modbus element at address `1000` and the Channel _ElectricityMeter.ChannelId.ACTIVE_POWER_. The Modbus element is defined as a 16 bit word element with an signed integer value.
+
[NOTE]
====
Expand All @@ -393,7 +393,7 @@ NOTE: Most Modbus function codes are available by their respective _FC*_ impleme
- For more advanced channel-to-element mapping functionalities the internal *cm()* method can be used - e.g. to map one Modbus element to multiple Channels.
+
Using this principle a complete Modbus table consisting of multiple register blocks that need to be read or written with different Modbus function codes can be defined. For details have a look at the existing implementation classes inside the Modbus Bridge source code.
<17> The SymmetricMeter Nature requires us to provide a *MeterType* via a `MeterType getMeterType()` method. The MeterType is provided by the Config.
<17> The ElectricityMeter Nature requires us to provide a *MeterType* via a `MeterType getMeterType()` method. The MeterType is provided by the Config.
<18> Finally it is always a good idea to define a *debugLog()* method. This method is called in each cycle by the *Controller.Debug.Log* and very helpful for continuous debugging.
====

Expand Down
4 changes: 0 additions & 4 deletions io.openems.edge.batteryinverter.sunspec/readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,4 @@ Generic implementation of SunSpec PV inverters. It is tested with
- KACO blueplanet TL.3 series
- SolarEdge SE12.5K - SE27.6K

Implemented Natures::
- ManagedSymmetricPvInverter
- SymmetricMeter

https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.pvinverter.sunspec[Source Code icon:github[]]
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import io.openems.edge.common.channel.Doc;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.meter.api.SymmetricMeter;
import io.openems.edge.meter.api.ElectricityMeter;

public interface BoschBpts5HybridMeter extends SymmetricMeter, OpenemsComponent {
public interface BoschBpts5HybridMeter extends ElectricityMeter, OpenemsComponent {

public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
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.meter.api.ElectricityMeter;
import io.openems.edge.meter.api.MeterType;
import io.openems.edge.meter.api.SymmetricMeter;

@Designate(ocd = Config.class, factory = true)
@Component(//
Expand All @@ -35,7 +35,7 @@
EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE, //
})
public class BoschBpts5HybridMeterImpl extends AbstractOpenemsComponent
implements BoschBpts5HybridMeter, SymmetricMeter, OpenemsComponent {
implements BoschBpts5HybridMeter, ElectricityMeter, OpenemsComponent {

@Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY)
private BoschBpts5HybridCore core;
Expand All @@ -46,7 +46,7 @@ public class BoschBpts5HybridMeterImpl extends AbstractOpenemsComponent
public BoschBpts5HybridMeterImpl() {
super(//
OpenemsComponent.ChannelId.values(), //
SymmetricMeter.ChannelId.values(), //
ElectricityMeter.ChannelId.values(), //
BoschBpts5HybridMeter.ChannelId.values() //
);
}
Expand Down
34 changes: 17 additions & 17 deletions io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Grid: Active Power.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter))
* <li>Interface: Sum (origin: ElectricityMeter))
* <li>Type: Integer
* <li>Unit: W
* <li>Range: negative values for Consumption (power that is 'leaving the
Expand All @@ -173,7 +173,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Grid: Active Power L1.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter / AsymmetricMeter)
* <li>Interface: Sum (origin: ElectricityMeter)
* <li>Type: Integer
* <li>Unit: W
* <li>Range: negative values for Consumption (power that is 'leaving the
Expand All @@ -190,7 +190,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Grid: Active Power L2.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter / AsymmetricMeter)
* <li>Interface: Sum (origin: ElectricityMeter)
* <li>Type: Integer
* <li>Unit: W
* <li>Range: negative values for Consumption (power that is 'leaving the
Expand All @@ -207,7 +207,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Grid: Active Power L3.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter / AsymmetricMeter)
* <li>Interface: Sum (origin: ElectricityMeter)
* <li>Type: Integer
* <li>Unit: W
* <li>Range: negative values for Consumption (power that is 'leaving the
Expand All @@ -224,7 +224,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Grid: Minimum Ever Active Power.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter))
* <li>Interface: Sum (origin: ElectricityMeter))
* <li>Type: Integer
* <li>Unit: W
* <li>Range: negative values or '0'
Expand All @@ -237,7 +237,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Grid: Maximum Ever Active Power.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter)
* <li>Interface: Sum (origin: ElectricityMeter)
* <li>Type: Integer
* <li>Unit: W
* <li>Range: positive values or '0'
Expand All @@ -250,7 +250,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Production: Active Power.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter and ESS DC Charger)
* <li>Interface: Sum (origin: ElectricityMeter and ESS DC Charger)
* <li>Type: Integer
* <li>Unit: W
* <li>Range: should be only positive
Expand All @@ -264,7 +264,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Production: AC Active Power.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter)
* <li>Interface: Sum (origin: ElectricityMeter)
* <li>Type: Integer
* <li>Unit: W
* <li>Range: should be only positive
Expand All @@ -278,7 +278,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Production: AC Active Power L1.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter / AsymmetricMeter)
* <li>Interface: Sum (origin: ElectricityMeter)
* <li>Type: Integer
* <li>Unit: W
* <li>Range: should be only positive
Expand All @@ -292,7 +292,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Production: AC Active Power L2.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter / AsymmetricMeter)
* <li>Interface: Sum (origin: ElectricityMeter)
* <li>Type: Integer
* <li>Unit: W
* <li>Range: should be only positive
Expand All @@ -306,7 +306,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Production: AC Active Power L3.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter / AsymmetricMeter)
* <li>Interface: Sum (origin: ElectricityMeter)
* <li>Type: Integer
* <li>Unit: W
* <li>Range: should be only positive
Expand Down Expand Up @@ -334,7 +334,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Production: Maximum Ever Active Power.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter))
* <li>Interface: Sum (origin: ElectricityMeter))
* <li>Type: Integer
* <li>Unit: W
* <li>Range: positive values or '0'
Expand All @@ -347,7 +347,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Production: Maximum Ever AC Active Power.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter))
* <li>Interface: Sum (origin: ElectricityMeter))
* <li>Type: Integer
* <li>Unit: W
* <li>Range: positive values or '0'
Expand Down Expand Up @@ -518,7 +518,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Grid: Buy-from-grid Energy ("Production").
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter)
* <li>Interface: Sum (origin: ElectricityMeter)
* <li>Type: Integer
* <li>Unit: Wh_Σ
* </ul>
Expand All @@ -530,7 +530,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Grid: Sell-to-grid Energy ("Consumption").
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter)
* <li>Interface: Sum (origin: ElectricityMeter)
* <li>Type: Long
* <li>Unit: Wh_Σ
* </ul>
Expand All @@ -553,7 +553,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Production: AC Energy.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter)
* <li>Interface: Sum (origin: ElectricityMeter)
* <li>Type: Long
* <li>Unit: Wh_Σ
* </ul>
Expand All @@ -577,7 +577,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
* Consumption: Energy.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricMeter)
* <li>Interface: Sum (origin: ElectricityMeter)
* <li>Type: Long
* <li>Unit: Wh_Σ
* </ul>
Expand Down
4 changes: 2 additions & 2 deletions io.openems.edge.controller.api.rest/readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ The `GET` api also understands regular expressions. Send a GET request to http:/
"address":"pvInverter0/ActivePower",
"type":"INTEGER",
"accessMode":"RO",
"text":"Negative values for Consumption; positive for Production",
"text":"",
"unit":"W",
"value":90
},
Expand All @@ -59,7 +59,7 @@ The `GET` api also understands regular expressions. Send a GET request to http:/
"address":"meter0/ActivePower",
"type":"INTEGER",
"accessMode":"RO",
"text":"Negative values for Consumption; positive for Production",
"text":"",
"unit":"W",
"value":465
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import io.openems.edge.ess.power.api.PowerException;
import io.openems.edge.ess.power.api.Pwr;
import io.openems.edge.ess.power.api.Relationship;
import io.openems.edge.meter.api.AsymmetricMeter;
import io.openems.edge.meter.api.ElectricityMeter;

@Designate(ocd = Config.class, factory = true)
@Component(//
Expand Down Expand Up @@ -71,7 +71,7 @@ protected void deactivate() {

@Override
public void run() throws OpenemsNamedException {
AsymmetricMeter meter = this.componentManager.getComponent(this.meterId);
ElectricityMeter meter = this.componentManager.getComponent(this.meterId);
ManagedAsymmetricEss ess = this.componentManager.getComponent(this.essId);

this.addConstraint(ess, Phase.L1, meter.getActivePowerL1(), meter.getReactivePowerL1(), ess.getActivePowerL1(),
Expand Down
Loading

0 comments on commit 5e7d18c

Please sign in to comment.