diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index 26bb687f19a..30957061722 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -164,6 +164,7 @@ bnd.identity;id='io.openems.edge.meter.carlo.gavazzi',\ bnd.identity;id='io.openems.edge.meter.discovergy',\ bnd.identity;id='io.openems.edge.meter.eastron',\ + bnd.identity;id='io.openems.edge.meter.hager',\ bnd.identity;id='io.openems.edge.meter.janitza',\ bnd.identity;id='io.openems.edge.meter.kdk',\ bnd.identity;id='io.openems.edge.meter.phoenixcontact',\ @@ -356,6 +357,7 @@ io.openems.edge.meter.carlo.gavazzi;version=snapshot,\ io.openems.edge.meter.discovergy;version=snapshot,\ io.openems.edge.meter.eastron;version=snapshot,\ + io.openems.edge.meter.hager;version=snapshot,\ io.openems.edge.meter.janitza;version=snapshot,\ io.openems.edge.meter.kdk;version=snapshot,\ io.openems.edge.meter.phoenixcontact;version=snapshot,\ diff --git a/io.openems.edge.controller.api.backend/src/io/openems/edge/controller/api/backend/WebsocketClient.java b/io.openems.edge.controller.api.backend/src/io/openems/edge/controller/api/backend/WebsocketClient.java index 10b7a380eab..25799ae4805 100644 --- a/io.openems.edge.controller.api.backend/src/io/openems/edge/controller/api/backend/WebsocketClient.java +++ b/io.openems.edge.controller.api.backend/src/io/openems/edge/controller/api/backend/WebsocketClient.java @@ -33,8 +33,9 @@ protected WebsocketClient(ControllerApiBackendImpl parent, String name, URI serv this.onNotification = new OnNotification(parent); this.onError = new OnError(parent); this.onClose = (ws, code, reason, remote) -> { - this.log.error("Disconnected from OpenEMS Backend [" + serverUri.toString() // - + (proxy != AbstractWebsocketClient.NO_PROXY ? " via Proxy" : "") + "]"); + this.log.error("Disconnected from OpenEMS Backend [ uri={}, proxy={}" // + + ", code={}, reason={}, remote={} ]", // + serverUri.toString(), proxy, code, reason, remote); this.parent.getUnableToSendChannel().setNextValue(true); }; } diff --git a/io.openems.edge.meter.hager/.classpath b/io.openems.edge.meter.hager/.classpath new file mode 100644 index 00000000000..6b893954e43 --- /dev/null +++ b/io.openems.edge.meter.hager/.classpath @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/io.openems.edge.meter.hager/.gitignore b/io.openems.edge.meter.hager/.gitignore new file mode 100644 index 00000000000..9e0adcc107c --- /dev/null +++ b/io.openems.edge.meter.hager/.gitignore @@ -0,0 +1 @@ +/generated/ diff --git a/io.openems.edge.meter.hager/.project b/io.openems.edge.meter.hager/.project new file mode 100644 index 00000000000..0f0db1fa9e5 --- /dev/null +++ b/io.openems.edge.meter.hager/.project @@ -0,0 +1,23 @@ + + + io.openems.edge.meter.hager + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/io.openems.edge.meter.hager/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.meter.hager/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/io.openems.edge.meter.hager/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/io.openems.edge.meter.hager/.settings/org.eclipse.jdt.core.prefs b/io.openems.edge.meter.hager/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..9a7984bb6c3 --- /dev/null +++ b/io.openems.edge.meter.hager/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,11 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=21 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=21 diff --git a/io.openems.edge.meter.hager/bnd.bnd b/io.openems.edge.meter.hager/bnd.bnd new file mode 100644 index 00000000000..99534c518f7 --- /dev/null +++ b/io.openems.edge.meter.hager/bnd.bnd @@ -0,0 +1,16 @@ +Bundle-Name: OpenEMS Edge Meter Hager +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.modbus,\ + io.openems.edge.common,\ + io.openems.edge.meter.api,\ + io.openems.j2mod,\ + + +-testpath: \ + ${testpath} diff --git a/io.openems.edge.meter.hager/src/io/openems/edge/meter/hager/ecr380d/Config.java b/io.openems.edge.meter.hager/src/io/openems/edge/meter/hager/ecr380d/Config.java new file mode 100644 index 00000000000..0d8748531c4 --- /dev/null +++ b/io.openems.edge.meter.hager/src/io/openems/edge/meter/hager/ecr380d/Config.java @@ -0,0 +1,35 @@ +package io.openems.edge.meter.hager.ecr380d; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +import io.openems.common.types.MeterType; + +@ObjectClassDefinition(// + name = "Meter Hager ECR380D", // + description = "Implements the Hager ECR380D meter.") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "meter0"; + + @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 = "Meter-Type", description = "What is measured by this Meter?") + MeterType type() default MeterType.PRODUCTION; + + @AttributeDefinition(name = "Modbus-ID", description = "ID of Modbus bridge.") + String modbus_id() default "modbus0"; + + @AttributeDefinition(name = "Modbus Unit-ID", description = "The Unit-ID of the Modbus device.") + int modbusUnitId(); + + @AttributeDefinition(name = "Modbus target filter", description = "This is auto-generated by 'Modbus-ID'.") + String Modbus_target() default "(enabled=true)"; + + String webconsole_configurationFactory_nameHint() default "Meter Hager ECR380D [{id}]"; +} diff --git a/io.openems.edge.meter.hager/src/io/openems/edge/meter/hager/ecr380d/HagerEcr380dMeter.java b/io.openems.edge.meter.hager/src/io/openems/edge/meter/hager/ecr380d/HagerEcr380dMeter.java new file mode 100644 index 00000000000..e58107cdbeb --- /dev/null +++ b/io.openems.edge.meter.hager/src/io/openems/edge/meter/hager/ecr380d/HagerEcr380dMeter.java @@ -0,0 +1,162 @@ +package io.openems.edge.meter.hager.ecr380d; + +import io.openems.common.channel.AccessMode; +import io.openems.common.channel.PersistencePriority; +import io.openems.common.channel.Unit; +import io.openems.common.types.OpenemsType; +import io.openems.edge.bridge.modbus.api.ElementToChannelConverter; +import io.openems.edge.bridge.modbus.api.ElementToChannelScaleFactorConverter; +import io.openems.edge.bridge.modbus.api.element.ModbusRegisterElement; +import io.openems.edge.bridge.modbus.api.element.SignedWordElement; +import io.openems.edge.bridge.modbus.api.element.StringWordElement; +import io.openems.edge.bridge.modbus.api.element.UnsignedDoublewordElement; +import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.meter.api.ElectricityMeter; + +/** + * Hager ECR380D Modbus-Register and scaling described at Modbus + * table Summary – rev. 1.14. + * + */ +public interface HagerEcr380dMeter extends ElectricityMeter, OpenemsComponent { + + /** + * Chapter 5. Device Identification (start 0x1000) + */ + public static final int DEVICE_START_ADDRESS = 0x1000; + /** + * Chapter 6. Instantaneous Measures (Start 0xB000) + */ + public static final int INSTANTANEOUS_MEASURES_START_ADDRESS = 0xB000; + /** + * Chapter 7. Energies (kWh, ΣT) – Start 0xB060 + */ + public static final int ENERGY_START_ADDRESS = 0xB060; + /** + * Chapter 12. Energies per Phase (kWh) + */ + public static final int ENERGY_PER_PHASE_START_ADDRESS = 0xB180; + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + // 5. Device Identification (start 0x1000) + VENDOR_NAME(Doc.of(OpenemsType.STRING).accessMode(AccessMode.READ_ONLY).persistencePriority(PersistencePriority.VERY_LOW).text("Vendor Name"), new StringWordElement(DEVICE_START_ADDRESS | 0x0000, 16), ElementToChannelConverter.DIRECT_1_TO_1), + PRODUCT_CODE(Doc.of(OpenemsType.STRING).accessMode(AccessMode.READ_ONLY).persistencePriority(PersistencePriority.VERY_LOW).text("Product Code"), new StringWordElement(DEVICE_START_ADDRESS | 0x0010, 16), ElementToChannelConverter.DIRECT_1_TO_1), + SW_VERSION(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).persistencePriority(PersistencePriority.VERY_LOW).text("Software version"), new StringWordElement(DEVICE_START_ADDRESS | 0x0020, 2), ElementToChannelConverter.DIRECT_1_TO_1), + VENDOR_URL(Doc.of(OpenemsType.STRING).accessMode(AccessMode.READ_ONLY).persistencePriority(PersistencePriority.VERY_LOW).text("Product Name"), new StringWordElement(DEVICE_START_ADDRESS | 0x0022, 16), ElementToChannelConverter.DIRECT_1_TO_1), + PRODUCT_NAME(Doc.of(OpenemsType.STRING).accessMode(AccessMode.READ_ONLY).persistencePriority(PersistencePriority.VERY_LOW).text("Product Name"), new StringWordElement(DEVICE_START_ADDRESS | 0x0032, 16), ElementToChannelConverter.DIRECT_1_TO_1), + MODEL_NAME(Doc.of(OpenemsType.STRING).accessMode(AccessMode.READ_ONLY).persistencePriority(PersistencePriority.VERY_LOW).text("Model Name"), new StringWordElement(DEVICE_START_ADDRESS | 0x0042, 16), ElementToChannelConverter.DIRECT_1_TO_1), + APPLICATION_NAME(Doc.of(OpenemsType.STRING).accessMode(AccessMode.READ_ONLY).persistencePriority(PersistencePriority.VERY_LOW).text("User application name"), new StringWordElement(DEVICE_START_ADDRESS | 0x0052, 16), ElementToChannelConverter.DIRECT_1_TO_1), + HW_VERSION(Doc.of(OpenemsType.STRING).accessMode(AccessMode.READ_ONLY).persistencePriority(PersistencePriority.VERY_LOW).text("Hardware version"), new StringWordElement(DEVICE_START_ADDRESS | 0x0062, 2), ElementToChannelConverter.DIRECT_1_TO_1), + PRODUCTION_CODE_SERIAL_NUMBER(Doc.of(OpenemsType.STRING).accessMode(AccessMode.READ_ONLY).persistencePriority(PersistencePriority.VERY_LOW).text("Production code serial number"), new StringWordElement(DEVICE_START_ADDRESS | 0x0064, 16), ElementToChannelConverter.DIRECT_1_TO_1), + PRODUCTION_SITE_CODE(Doc.of(OpenemsType.STRING).accessMode(AccessMode.READ_ONLY).persistencePriority(PersistencePriority.VERY_LOW).text("Production site code"), new StringWordElement(DEVICE_START_ADDRESS | 0x0074, 2), ElementToChannelConverter.DIRECT_1_TO_1), + PRODUCTION_DAY_OF_YEAR(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).persistencePriority(PersistencePriority.VERY_LOW).text("Production day of the year"), new UnsignedWordElement(DEVICE_START_ADDRESS | 0x0076), ElementToChannelConverter.DIRECT_1_TO_1), + PRODUCTION_YEAR(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).persistencePriority(PersistencePriority.VERY_LOW).text("Production year"), new UnsignedWordElement(DEVICE_START_ADDRESS | 0x0077), ElementToChannelConverter.DIRECT_1_TO_1), + + // 6. Instantaneous Measures (Start 0xB000) + // V_L1_NEUTRAL(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT).persistencePriority(PersistencePriority.HIGH).text("Power L1 to N"),new UnsignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0000), ElementToChannelScaleFactorConverter.DIVIDE(100.0d)), + // V_L2_NEUTRAL(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT).persistencePriority(PersistencePriority.HIGH).text("Power L2 to N"), new UnsignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0001), ElementToChannelScaleFactorConverter.DIVIDE(100.0d)), + // V_L3_NEUTRAL(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT).persistencePriority(PersistencePriority.HIGH).text("Power L3 to N"), new UnsignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0002), ElementToChannelScaleFactorConverter.DIVIDE(100.0d)), + V_L1_L2(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.MILLIVOLT).persistencePriority(PersistencePriority.HIGH).text("Power L1 to L2"), new UnsignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0003), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), + V_L2_L3(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.MILLIVOLT).persistencePriority(PersistencePriority.HIGH).text("Power L2 to L3"), new UnsignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0004), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), + V_L3_L1(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.MILLIVOLT).persistencePriority(PersistencePriority.HIGH).text("Power L3 to L1"), new UnsignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0005), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), + // FREQUENCY(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.HERTZ).persistencePriority(PersistencePriority.HIGH).text("Frequency"), new UnsignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0006), ElementToChannelScaleFactorConverter.DIVIDE(100.0d)), + /* 0x0007+8 not defined */ + // I_L1(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.MILLIAMPERE).persistencePriority(PersistencePriority.HIGH).text("Current L1"), new UnsignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0009), ElementToChannelConverter.DIRECT_1_TO_1), + // I_L2(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.MILLIAMPERE).persistencePriority(PersistencePriority.HIGH).text("Current L2"), new UnsignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x000B), ElementToChannelConverter.DIRECT_1_TO_1), + // I_L3(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.MILLIAMPERE).persistencePriority(PersistencePriority.HIGH).text("Current L3"), new UnsignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x000D), ElementToChannelConverter.DIRECT_1_TO_1), + I_NEUTRAL(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.MILLIAMPERE).persistencePriority(PersistencePriority.HIGH).text("Current neutral"), new UnsignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x000F), ElementToChannelConverter.DIRECT_1_TO_1), + + // P_SUM(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.WATT).persistencePriority(PersistencePriority.HIGH).text("Power Sum(L1,L2,L3)"), new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0011), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // kW/100, S32 + // Q_SUM(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE_REACTIVE).persistencePriority(PersistencePriority.HIGH).text("Power reactive Sum(L1,L2,L3)"), new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0013), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // kvar/100, S32 + S_SUM(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE).persistencePriority(PersistencePriority.HIGH).text("Power apparent Sum(L1,L2,L3)"), new UnsignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0015), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // kVA/100, U32 + PF_SUM_IEC(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.NONE).persistencePriority(PersistencePriority.HIGH).text("Power factor IEC Sum(L1,L2,L3)"), new SignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0017), ElementToChannelConverter.DIRECT_1_TO_1), // promille, S16, -1000..+1000 + PF_SUM_IEEE(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.NONE).persistencePriority(PersistencePriority.HIGH).text("Power factor IEEE Sum(L1,L2,L3)"), new SignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0018), ElementToChannelConverter.DIRECT_1_TO_1), // promille, S16, -1000..+1000 + + // P_L1(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.WATT).persistencePriority(PersistencePriority.HIGH).text("Power L1"), new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0019), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // kW/100, S32 + // P_L2(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.WATT).persistencePriority(PersistencePriority.HIGH).text("Power L2"), new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x001B), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // kW/100, S32 + // P_L3(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.WATT).persistencePriority(PersistencePriority.HIGH).text("Power L3"), new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x001D), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // kW/100, S32 + + // Q_L1(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE_REACTIVE).persistencePriority(PersistencePriority.HIGH).text("Power reactive L1"), new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x001F), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // kvar/100, S32 + // Q_L2(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE_REACTIVE).persistencePriority(PersistencePriority.HIGH).text("Power reactive L2"), new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0021), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // kvar/100, S32 + // Q_L3(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE_REACTIVE).persistencePriority(PersistencePriority.HIGH).text("Power reactive L3"), new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0023), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // kvar/100, S32 + + S_L1(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE).persistencePriority(PersistencePriority.HIGH).text("Power apparent L1"), new UnsignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0025), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // kVA/100, U32 + S_L2(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE).persistencePriority(PersistencePriority.HIGH).text("Power apparent L2"), new UnsignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0027), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // kVA/100, U32 + S_L3(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE).persistencePriority(PersistencePriority.HIGH).text("Power apparent L3"), new UnsignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0029), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // kVA/100, U32 + + PF_L1_IEC(Doc.of(OpenemsType.FLOAT).accessMode(AccessMode.READ_ONLY).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH).text("Power factor IEC L1"), new SignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x002B), ElementToChannelScaleFactorConverter.DIVIDE(1000.0d)), // promille, S16, -1000..+1000 + PF_L2_IEC(Doc.of(OpenemsType.FLOAT).accessMode(AccessMode.READ_ONLY).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH).text("Power factor IEC L2"), new SignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x002C), ElementToChannelScaleFactorConverter.DIVIDE(1000.0d)), // promille, S16, -1000..+1000 + PF_L3_IEC(Doc.of(OpenemsType.FLOAT).accessMode(AccessMode.READ_ONLY).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH).text("Power factor IEC L3"), new SignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x002D), ElementToChannelScaleFactorConverter.DIVIDE(1000.0d)), // promille, S16, -1000..+1000 + + PF_L1_IEEE(Doc.of(OpenemsType.FLOAT).accessMode(AccessMode.READ_ONLY).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH).text("Power factor IEEE L1"), new SignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x002E), ElementToChannelScaleFactorConverter.DIVIDE(1000.0d)), // promille, S16, -1000..+1000 + PF_L2_IEEE(Doc.of(OpenemsType.FLOAT).accessMode(AccessMode.READ_ONLY).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH).text("Power factor IEEE L2"), new SignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x002F), ElementToChannelScaleFactorConverter.DIVIDE(1000.0d)), // promille, S16, -1000..+1000 + PF_L3_IEEE(Doc.of(OpenemsType.FLOAT).accessMode(AccessMode.READ_ONLY).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH).text("Power factor IEEE L3"), new SignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0030), ElementToChannelScaleFactorConverter.DIVIDE(1000.0d)), // promille, S16, -1000..+1000 + + // 7. Energies (kWh, ΣT) – Start 0xB060 + // EA_PLUS_SUM(Doc.of(OpenemsType.LONG).unit(Unit.WATT_HOURS).persistencePriority(PersistencePriority.HIGH).text("Imported Energy"), new UnsignedDoublewordElement(ENERGY_START_ADDRESS | 0x0000), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // Ea+ (ΣT) kWh, U32 + ER_PLUS_SUM(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE_REACTIVE_HOURS).persistencePriority(PersistencePriority.HIGH).text("Imported reactive Energy"), new UnsignedDoublewordElement(ENERGY_START_ADDRESS | 0x0002), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // Ea+ (ΣT) kWh, U32 + // EA_MINUS_SUM(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.WATT_HOURS).persistencePriority(PersistencePriority.HIGH).text("Exported Energy"), new UnsignedDoublewordElement(ENERGY_START_ADDRESS | 0x0004), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // Ea- (ΣT) kWh, U32 + ER_MINUS_SUM(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE_REACTIVE_HOURS).persistencePriority(PersistencePriority.HIGH).text("Exported reactive Energy"), new UnsignedDoublewordElement(ENERGY_START_ADDRESS | 0x0006), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // Ea- (ΣT) kWh, U32 + EA_PLUS_DETAILED_SUM(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.WATT_HOURS).persistencePriority(PersistencePriority.HIGH).text("Imported Energy"), new UnsignedDoublewordElement(ENERGY_START_ADDRESS | 0x0008), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // Ea+ (ΣT) kWh, U32 + EA_MINUS_DETAILED_SUM(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.WATT_HOURS).persistencePriority(PersistencePriority.HIGH).text("Exported Energy"), new UnsignedDoublewordElement(ENERGY_START_ADDRESS | 0x000A), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // Ea- (ΣT) kWh, U32 + + // 12. Energies per Phase (kWh) + // EA_PLUS_L1(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.WATT_HOURS).persistencePriority(PersistencePriority.HIGH).text("Imported Energy L1"), new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0000), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), + // EA_PLUS_L2(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.WATT_HOURS).persistencePriority(PersistencePriority.HIGH).text("Imported Energy L2"), new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0002), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), + // EA_PLUS_L3(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.WATT_HOURS).persistencePriority(PersistencePriority.HIGH).text("Imported Energy L3"), new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0004), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), + // EA_MINUS_L1(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.WATT_HOURS).persistencePriority(PersistencePriority.HIGH).text("Exported Energy L1"), new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0006), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), + // EA_MINUS_L2(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.WATT_HOURS).persistencePriority(PersistencePriority.HIGH).text("Exported Energy L2"), new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0008), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), + // EA_MINUS_L3(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.WATT_HOURS).persistencePriority(PersistencePriority.HIGH).text("Exported Energy L3"), new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x000A), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), + + ER_PLUS_L1(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE_REACTIVE_HOURS).persistencePriority(PersistencePriority.HIGH).text("Imported reactive Energy L1"), new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x000C), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), + ER_PLUS_L2(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE_REACTIVE_HOURS).persistencePriority(PersistencePriority.HIGH).text("Imported reactive Energy L2"), new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x000E), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), + ER_PLUS_L3(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE_REACTIVE_HOURS).persistencePriority(PersistencePriority.HIGH).text("Imported reactive Energy L3"), new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0010), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), + ER_MINUS_L1(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE_REACTIVE_HOURS).persistencePriority(PersistencePriority.HIGH).text("Exported reactive Energy L1"), new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0012), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), + ER_MINUS_L2(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE_REACTIVE_HOURS).persistencePriority(PersistencePriority.HIGH).text("Exported reactive Energy L2"), new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0014), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), + ER_MINUS_L3(Doc.of(OpenemsType.LONG).accessMode(AccessMode.READ_ONLY).unit(Unit.VOLT_AMPERE_REACTIVE_HOURS).persistencePriority(PersistencePriority.HIGH).text("Exported reactive Energy L3"), new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0016), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), + + ; + + private final Doc doc; + private final ModbusRegisterElement address; + private final ElementToChannelConverter converter; + + ChannelId(final Doc doc, final ModbusRegisterElement address, ElementToChannelConverter converter) { + this.doc = doc; + this.address = address; + this.converter = converter; + } + + /** + * Accessor method for the {@link Doc} instance of this channel. + * + * @return the Doc instance + * @see ChannelId#doc() + */ + @Override + public Doc doc() { + return this.doc; + } + + /** + * Accessor method for the modbus register number of this channel. + * + * @return the modbus register number for this channel + */ + public ModbusRegisterElement address() { + return this.address; + } + + /** + * Accessor method for the modbus channel converter of this channel. + * + * @return the converter for this channel + */ + public ElementToChannelConverter converter() { + return this.converter; + } + } +} \ No newline at end of file diff --git a/io.openems.edge.meter.hager/src/io/openems/edge/meter/hager/ecr380d/HagerEcr380dMeterImpl.java b/io.openems.edge.meter.hager/src/io/openems/edge/meter/hager/ecr380d/HagerEcr380dMeterImpl.java new file mode 100644 index 00000000000..eb2d77183c6 --- /dev/null +++ b/io.openems.edge.meter.hager/src/io/openems/edge/meter/hager/ecr380d/HagerEcr380dMeterImpl.java @@ -0,0 +1,252 @@ +package io.openems.edge.meter.hager.ecr380d; + +import org.osgi.service.cm.ConfigurationAdmin; +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.Modified; +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.metatype.annotations.Designate; + +import io.openems.common.channel.AccessMode; +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; +import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; +import io.openems.edge.bridge.modbus.api.BridgeModbus; +import io.openems.edge.bridge.modbus.api.ElementToChannelConverter; +import io.openems.edge.bridge.modbus.api.ElementToChannelScaleFactorConverter; +import io.openems.edge.bridge.modbus.api.ModbusComponent; +import io.openems.edge.bridge.modbus.api.ModbusProtocol; +import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement; +import io.openems.edge.bridge.modbus.api.element.ModbusRegisterElement; +import io.openems.edge.bridge.modbus.api.element.SignedDoublewordElement; +import io.openems.edge.bridge.modbus.api.element.UnsignedDoublewordElement; +import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement; +import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask; +import io.openems.edge.common.channel.value.Value; +import io.openems.edge.common.component.OpenemsComponent; +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.modbusslave.ModbusType; +import io.openems.edge.common.taskmanager.Priority; +import io.openems.edge.meter.api.ElectricityMeter; + +/** + * Hager ECR380D + * energy meter via Modbus RTU. + * + * @see HagerEcr380dMeter for detailed mapping information + */ +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Meter.Hager.ECR380D", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class HagerEcr380dMeterImpl extends AbstractOpenemsModbusComponent // + implements HagerEcr380dMeter, ElectricityMeter, ModbusComponent, OpenemsComponent, ModbusSlave { + + @Reference + private ConfigurationAdmin cm; + + @Override + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + protected void setModbus(BridgeModbus modbus) { + super.setModbus(modbus); + } + + private Config config; + + public HagerEcr380dMeterImpl() { + super(OpenemsComponent.ChannelId.values(), // + ModbusComponent.ChannelId.values(), // + ElectricityMeter.ChannelId.values(), // + HagerEcr380dMeter.ChannelId.values()// + ); + } + + @Activate + @Modified + protected void activate(final ComponentContext context, final Config config) throws OpenemsException { + this.config = config; + + super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId(), this.cm, "Modbus", config.modbus_id()); + } + + + @Deactivate + @Override + protected void deactivate() { + super.deactivate(); + } + + @Override + public String debugLog() { + final Value p = this.getActivePower(); + return "P=" + (p.isDefined() ? p.get() + " W" : "-"); + } + + @Override + public MeterType getMeterType() { + return this.config.type(); + } + + /** + * Calculate a List of registers, this modbus slave provides to the + * outside world. + * + * @param accessMode the filter which registers are available + * @return the table of registers, this implementation present to clients + */ + @Override + public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { + return new ModbusSlaveTable(// + OpenemsComponent.getModbusSlaveNatureTable(accessMode), // + ElectricityMeter.getModbusSlaveNatureTable(accessMode), // + ModbusSlaveNatureTable.of(HagerEcr380dMeter.class, accessMode, 0x002C) // + .channel(0x0000, HagerEcr380dMeter.ChannelId.V_L1_L2, ModbusType.UINT32) // + .channel(0x0002, HagerEcr380dMeter.ChannelId.V_L2_L3, ModbusType.UINT32) // + .channel(0x0004, HagerEcr380dMeter.ChannelId.V_L3_L1, ModbusType.UINT32) // + .channel(0x0006, HagerEcr380dMeter.ChannelId.ER_PLUS_L1, ModbusType.UINT32) // + .channel(0x0008, HagerEcr380dMeter.ChannelId.I_NEUTRAL, ModbusType.UINT64) // + .channel(0x000C, HagerEcr380dMeter.ChannelId.ER_PLUS_SUM, ModbusType.UINT64) // + .channel(0x0010, HagerEcr380dMeter.ChannelId.ER_MINUS_SUM, ModbusType.UINT64) // + .channel(0x0014, HagerEcr380dMeter.ChannelId.ER_PLUS_L1, ModbusType.UINT64) // + .channel(0x0018, HagerEcr380dMeter.ChannelId.ER_PLUS_L2, ModbusType.UINT64) // + .channel(0x001C, HagerEcr380dMeter.ChannelId.ER_PLUS_L3, ModbusType.UINT64) // + .channel(0x0020, HagerEcr380dMeter.ChannelId.ER_MINUS_L1, ModbusType.UINT64) // + .channel(0x0024, HagerEcr380dMeter.ChannelId.ER_MINUS_L2, ModbusType.UINT64) // + .channel(0x0028, HagerEcr380dMeter.ChannelId.ER_MINUS_L3, ModbusType.UINT64) // + .build() + ); + } + + /** + * Calculate the read commands used to read values from the device. + * + * @return the modbus protocol to requests value from the device + */ + @Override + protected ModbusProtocol defineModbusProtocol() { + final ModbusProtocol modbusProtocol = new ModbusProtocol(this); + + modbusProtocol.addTask(this.getInstantaneousMeasuresTask()); + + modbusProtocol.addTask(this.getEnergyTask()); + + modbusProtocol.addTask(this.getEnergyByPhaseTask()); + + modbusProtocol.addTask(this.getDeviceTask()); + + return modbusProtocol; + } + + private FC3ReadRegistersTask getDeviceTask() { + return new FC3ReadRegistersTask(// + DEVICE_START_ADDRESS, // + Priority.LOW, + this.m(HagerEcr380dMeter.ChannelId.VENDOR_NAME), // + this.m(HagerEcr380dMeter.ChannelId.PRODUCT_CODE), // + this.m(HagerEcr380dMeter.ChannelId.SW_VERSION), // + this.m(HagerEcr380dMeter.ChannelId.VENDOR_URL), // + this.m(HagerEcr380dMeter.ChannelId.PRODUCT_NAME), // + this.m(HagerEcr380dMeter.ChannelId.MODEL_NAME), // + this.m(HagerEcr380dMeter.ChannelId.APPLICATION_NAME), // + this.m(HagerEcr380dMeter.ChannelId.HW_VERSION), // + this.m(HagerEcr380dMeter.ChannelId.PRODUCTION_CODE_SERIAL_NUMBER), // + this.m(HagerEcr380dMeter.ChannelId.PRODUCTION_SITE_CODE), // + this.m(HagerEcr380dMeter.ChannelId.PRODUCTION_DAY_OF_YEAR), // + this.m(HagerEcr380dMeter.ChannelId.PRODUCTION_YEAR) // + ); + } + + private FC3ReadRegistersTask getInstantaneousMeasuresTask() { + return new FC3ReadRegistersTask(// + INSTANTANEOUS_MEASURES_START_ADDRESS, // + Priority.HIGH, + this.m(ElectricityMeter.ChannelId.VOLTAGE_L1, new UnsignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0000), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), + this.m(ElectricityMeter.ChannelId.VOLTAGE_L2, new UnsignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0001), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), + this.m(ElectricityMeter.ChannelId.VOLTAGE_L3, new UnsignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0002), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), + this.m(HagerEcr380dMeter.ChannelId.V_L1_L2), // + this.m(HagerEcr380dMeter.ChannelId.V_L2_L3), // + this.m(HagerEcr380dMeter.ChannelId.V_L3_L1), // + this.m(ElectricityMeter.ChannelId.FREQUENCY, new UnsignedWordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0006), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // + new DummyRegisterElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0007, INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0008), // + this.m(ElectricityMeter.ChannelId.CURRENT_L1, new UnsignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0009), ElementToChannelConverter.DIRECT_1_TO_1), // + this.m(ElectricityMeter.ChannelId.CURRENT_L2, new UnsignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x000B), ElementToChannelConverter.DIRECT_1_TO_1), // + this.m(ElectricityMeter.ChannelId.CURRENT_L3, new UnsignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x000D), ElementToChannelConverter.DIRECT_1_TO_1), // + this.m(HagerEcr380dMeter.ChannelId.I_NEUTRAL), // + this.m(ElectricityMeter.ChannelId.ACTIVE_POWER, new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0011), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // + this.m(ElectricityMeter.ChannelId.REACTIVE_POWER, new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0013), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // + this.m(HagerEcr380dMeter.ChannelId.S_SUM), // + this.m(HagerEcr380dMeter.ChannelId.PF_SUM_IEC), // + this.m(HagerEcr380dMeter.ChannelId.PF_SUM_IEEE), // + this.m(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0019), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // + this.m(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x001B), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // + this.m(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x001D), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // + this.m(ElectricityMeter.ChannelId.REACTIVE_POWER_L1, new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x001F), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // + this.m(ElectricityMeter.ChannelId.REACTIVE_POWER_L2, new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0021), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // + this.m(ElectricityMeter.ChannelId.REACTIVE_POWER_L3, new SignedDoublewordElement(INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0023), ElementToChannelScaleFactorConverter.MULTIPLY(10.0d)), // + this.m(HagerEcr380dMeter.ChannelId.S_L1), // + this.m(HagerEcr380dMeter.ChannelId.S_L2), // + this.m(HagerEcr380dMeter.ChannelId.S_L3), // + this.m(HagerEcr380dMeter.ChannelId.PF_L1_IEC), // + this.m(HagerEcr380dMeter.ChannelId.PF_L2_IEC), // + this.m(HagerEcr380dMeter.ChannelId.PF_L3_IEC), // + this.m(HagerEcr380dMeter.ChannelId.PF_L1_IEEE), // + this.m(HagerEcr380dMeter.ChannelId.PF_L2_IEEE), // + this.m(HagerEcr380dMeter.ChannelId.PF_L3_IEEE) // + ); + } + + private FC3ReadRegistersTask getEnergyTask() { + return new FC3ReadRegistersTask(// + ENERGY_START_ADDRESS, // + Priority.HIGH, // + this.m(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY, new UnsignedDoublewordElement(ENERGY_START_ADDRESS | 0x0000), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // + this.m(HagerEcr380dMeter.ChannelId.ER_PLUS_SUM), // + this.m(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, new UnsignedDoublewordElement(ENERGY_START_ADDRESS | 0x0004), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // + this.m(HagerEcr380dMeter.ChannelId.ER_MINUS_SUM), // + this.m(HagerEcr380dMeter.ChannelId.EA_PLUS_DETAILED_SUM), // + this.m(HagerEcr380dMeter.ChannelId.EA_MINUS_DETAILED_SUM) // + ); + } + + private FC3ReadRegistersTask getEnergyByPhaseTask() { + return new FC3ReadRegistersTask(// + ENERGY_PER_PHASE_START_ADDRESS, // + Priority.HIGH, // + this.m(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY_L1, new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0000), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // + this.m(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY_L2, new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0002), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // + this.m(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY_L3, new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0004), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // + this.m(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY_L1, new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0006), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // + this.m(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY_L2, new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x0008), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // + this.m(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY_L3, new UnsignedDoublewordElement(ENERGY_PER_PHASE_START_ADDRESS | 0x000A), ElementToChannelScaleFactorConverter.MULTIPLY(1000.0d)), // + this.m(HagerEcr380dMeter.ChannelId.ER_PLUS_L1), + this.m(HagerEcr380dMeter.ChannelId.ER_PLUS_L2), + this.m(HagerEcr380dMeter.ChannelId.ER_PLUS_L3), + this.m(HagerEcr380dMeter.ChannelId.ER_MINUS_L1), + this.m(HagerEcr380dMeter.ChannelId.ER_MINUS_L2), + this.m(HagerEcr380dMeter.ChannelId.ER_MINUS_L3) + + ); + } + + /** + * Helper method to create a modbus register by a {@link HagerEcr380dMeter.ChannelId}. + * + * @param channelId The channel description + * @return the element parameter + */ + private ModbusRegisterElement m(HagerEcr380dMeter.ChannelId channelId) { + return channelId.converter() != null // + ? this.m(channelId, channelId.address(), channelId.converter()) // + : this.m(channelId, channelId.address()); + } +} diff --git a/io.openems.edge.meter.hager/src/io/openems/edge/meter/hager/ecr380d/package-info.java b/io.openems.edge.meter.hager/src/io/openems/edge/meter/hager/ecr380d/package-info.java new file mode 100644 index 00000000000..c151ae42842 --- /dev/null +++ b/io.openems.edge.meter.hager/src/io/openems/edge/meter/hager/ecr380d/package-info.java @@ -0,0 +1,7 @@ +/** + * Energy meter: Hager ECR 380 D. + * + * @author hrohmer + */ +@org.osgi.annotation.versioning.Version("1.0.0") +package io.openems.edge.meter.hager.ecr380d; \ No newline at end of file diff --git a/io.openems.edge.meter.hager/test/io/openems/edge/meter/hager/ecr380d/HagerEcr380dMeterTest.java b/io.openems.edge.meter.hager/test/io/openems/edge/meter/hager/ecr380d/HagerEcr380dMeterTest.java new file mode 100644 index 00000000000..35491eabab8 --- /dev/null +++ b/io.openems.edge.meter.hager/test/io/openems/edge/meter/hager/ecr380d/HagerEcr380dMeterTest.java @@ -0,0 +1,225 @@ +package io.openems.edge.meter.hager.ecr380d; + + +import static org.junit.Assert.assertTrue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import io.openems.common.types.MeterType; +import io.openems.edge.bridge.modbus.test.DummyModbusBridge; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.meter.api.ElectricityMeter; +import io.openems.edge.common.test.ComponentTest; + +/** + * minimal smoke test with {@link DummyModbusBridge}. + */ +public class HagerEcr380dMeterTest { + + private static final String CID = "meter0"; + private static final String MID = "modbus0"; + private static final MyConfig CONFIG = MyConfig.create() // + .setId(CID) + .setModbusId(MID) + .setType(MeterType.GRID) + .build(); + + private ComponentTest componentTest; + + @Before + public void before() throws Exception { + final DummyModbusBridge bridge = new DummyModbusBridge(MID) // + .withRegister(HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0020, 0x0203) + .withRegister(HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0021, 0x0000) + .withRegister(HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0062, 0x0401) + .withRegister(HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0063, 0x0000) + .withRegister(HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0076, 0x0001) + .withRegister(HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0077, 0x07E9) + + .withRegisters(HagerEcr380dMeter.INSTANTANEOUS_MEASURES_START_ADDRESS | 0x0000, // + 23000, // + 23150, // + 22950, // + 39500, // + 39600, // + 39000, // + 5010, // + 0x0000, // + 0x0000, // + 0x0000, 0x4E20,// + 0x0000, 0x9C40,// + 0x0000, 0xEA60,// + 0x0000, 0x2710,// + 0x0027, 0x1F3C,// P_SUM + 0x0008, 0xF494,// Q_SUM + 0x0001, 0x2624,// S_SUM + 0x024B, // PF_SUM_IEC + 0x035C, // PF_SUM_IEEE + 0x0003, 0x5E80, // ACTIVE_POWER_L1 + 0xFFFC, 0xA180, // ACTIVE_POWER_L2 + 0x0001, 0xAF40, // ACTIVE_POWER_L3 + 0x0003, 0x5E80, // REACTIVE_POWER_L1 + 0x0001, 0xAF40, // REACTIVE_POWER_L2 + 0xFFFE, 0x50C0, // REACTIVE_POWER_L3 + 0x0003, 0x5E80, // S_L1 + 0x0001, 0xAF40, // S_L2 + 0x0000, 0x0000, // S_L3 + 0x0000, // PF_L1_IEC + 0x0000, // PF_L1_IEC + 0x0000, // PF_L2_IEC + 0x0000, // PF_L1_IEEE + 0x0000, // PF_L2_IEEE + 0x0000 // PF_L3_IEEE + ) // + .withRegisters(HagerEcr380dMeter.ENERGY_START_ADDRESS | 0x0000, // + 0x0098, 0x967F,// ACTIVE_CONSUMPTION_ENERGY + 0x0032, 0xDCD5,// ER_PLUS_SUM + 0x0009, 0xFBF1,// ACTIVE_PRODUCTION_ENERGY + 0x0012, 0xD687,// ER_MINUS_SUM + 0x0023, 0xCACE,// EA_PLUS_DETAILED_SUM + 0x0011, 0x201E) // EA_MINUS_DETAILED_SUM + .withRegisters(HagerEcr380dMeter.ENERGY_PER_PHASE_START_ADDRESS | 0x0000, // + 0x0098, 0x961B,// ACTIVE_CONSUMPTION_ENERGY_L1 + 0x0098, 0x95B7,// ACTIVE_CONSUMPTION_ENERGY_L2 + 0x0098, 0x9553,// ACTIVE_CONSUMPTION_ENERGY_L3 + 0x0098, 0x9297,// ACTIVE_PRODUCTION_ENERGY_L1 + 0x0098, 0x8EAF,// ACTIVE_PRODUCTION_ENERGY_L2 + 0x0098, 0x8AC7,// ACTIVE_PRODUCTION_ENERGY_L3 + 0x0010, 0xF703,// ER_PLUS_L1 + 0x0010, 0xF69F,// ER_PLUS_L2 + 0x0010, 0xF63B,// ER_PLUS_L2 + 0x0054, 0xC68F,// ER_MINUS_L1 + 0x0054, 0xC62B,// ER_MINUS_L1 + 0x0054, 0xC5C7); // ER_MINUS_L1 + this.withRegister(bridge, HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0000, "Hager "); + this.withRegister(bridge, HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0010, "ECR380D "); + this.withRegister(bridge, HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0022, "http://www.hager.de "); + this.withRegister(bridge, HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0032, "3P Meter 80A 4M "); + this.withRegister(bridge, HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0042, "AGARDIO RJ45 MID "); + this.withRegister(bridge, HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0052, "APL "); + this.withRegister(bridge, HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0064, "12345678 "); + this.withRegister(bridge, HagerEcr380dMeter.DEVICE_START_ADDRESS | 0x0074, "DE "); + + this.componentTest = new ComponentTest(new HagerEcr380dMeterImpl()) + .addReference("cm", new io.openems.edge.common.test.DummyConfigurationAdmin()) + .addReference("setModbus", bridge); + } + + @After + public void after() throws Exception { + if (this.componentTest != null) { + this.componentTest.deactivate(); + } + this.componentTest = null; + } + + @Test + public void activateDeactivateTest() throws Exception { + this.componentTest.activate(CONFIG) + .next(new TestCase()) + .deactivate(); + + assertTrue(true); + } + + @Test + public void testVoltage() throws Exception { + this.componentTest.activate(CONFIG) + .next(new TestCase()// + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 230000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L2, 231500) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L3, 229500) // + .output(HagerEcr380dMeter.ChannelId.V_L1_L2, 395000) // + .output(HagerEcr380dMeter.ChannelId.V_L2_L3, 396000) // + .output(HagerEcr380dMeter.ChannelId.V_L3_L1, 390000) // + ) + .deactivate(); + } + + @Test + public void testFrequency() throws Exception { + this.componentTest.activate(CONFIG) + .next(new TestCase()// + .output(ElectricityMeter.ChannelId.FREQUENCY, 50100) // + ) + .deactivate(); + } + + @Test + public void testCurrent() throws Exception { + this.componentTest.activate(CONFIG) + .next(new TestCase()// + .output(ElectricityMeter.ChannelId.CURRENT_L1, 20000) // + .output(ElectricityMeter.ChannelId.CURRENT_L2, 40000) // + .output(ElectricityMeter.ChannelId.CURRENT_L3, 60000) // + .output(HagerEcr380dMeter.ChannelId.I_NEUTRAL, 10000) // + ) + .deactivate(); + } + + @Test + public void testPower() throws Exception { + this.componentTest.activate(CONFIG) + .next(new TestCase()// + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 25639000) // + .output(ElectricityMeter.ChannelId.REACTIVE_POWER, 5869000) // + .output(HagerEcr380dMeter.ChannelId.S_SUM, 753000) // + .output(HagerEcr380dMeter.ChannelId.PF_SUM_IEC, 587) // + .output(HagerEcr380dMeter.ChannelId.PF_SUM_IEEE, 860) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, 2208000) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, -2208000) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, 1104000) // + .output(ElectricityMeter.ChannelId.REACTIVE_POWER_L1, 2208000) + .output(ElectricityMeter.ChannelId.REACTIVE_POWER_L2, 1104000) + .output(ElectricityMeter.ChannelId.REACTIVE_POWER_L3, -1104000) + .output(HagerEcr380dMeter.ChannelId.S_L1, 2208000) // + .output(HagerEcr380dMeter.ChannelId.S_L2, 1104000) // + .output(HagerEcr380dMeter.ChannelId.S_L3, 0) // + ) + .deactivate(); + } + + @Test + public void testEnergy() throws Exception { + this.componentTest.activate(CONFIG) + .next(new TestCase()// + .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY, 9999999000L) // + .output(HagerEcr380dMeter.ChannelId.ER_PLUS_SUM, 3333333000L) // + .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, 654321000L) // + .output(HagerEcr380dMeter.ChannelId.ER_MINUS_SUM, 1234567000L) // + .output(HagerEcr380dMeter.ChannelId.EA_PLUS_DETAILED_SUM, 2345678000L) // + .output(HagerEcr380dMeter.ChannelId.EA_MINUS_DETAILED_SUM, 1122334000L) // + ) + .deactivate(); + } + + @Test + public void testEnergyPerPhase() throws Exception { + this.componentTest.activate(CONFIG) + .next(new TestCase()// + .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY_L1, 9999899000L) // + .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY_L2, 9999799000L) // + .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY_L3, 9999699000L) // + .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY_L1, 9998999000L) // + .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY_L2, 9997999000L) // + .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY_L3, 9996999000L) // + .output(HagerEcr380dMeter.ChannelId.ER_PLUS_L1, 1111811000L) // + .output(HagerEcr380dMeter.ChannelId.ER_PLUS_L2, 1111711000L) // + .output(HagerEcr380dMeter.ChannelId.ER_PLUS_L3, 1111611000L) // + .output(HagerEcr380dMeter.ChannelId.ER_MINUS_L1, 5555855000L) // + .output(HagerEcr380dMeter.ChannelId.ER_MINUS_L2, 5555755000L) // + .output(HagerEcr380dMeter.ChannelId.ER_MINUS_L3, 5555655000L) // + ) + .deactivate(); + } + + + private void withRegister(DummyModbusBridge bridge, int address, String value) { + final byte[] registers = value.getBytes(); + for (int i = 0; i < registers.length; i += 2) { + bridge.withRegister(address++, registers[i], registers[i + 1]); + } + } +} diff --git a/io.openems.edge.meter.hager/test/io/openems/edge/meter/hager/ecr380d/MyConfig.java b/io.openems.edge.meter.hager/test/io/openems/edge/meter/hager/ecr380d/MyConfig.java new file mode 100644 index 00000000000..4e54c59145c --- /dev/null +++ b/io.openems.edge.meter.hager/test/io/openems/edge/meter/hager/ecr380d/MyConfig.java @@ -0,0 +1,75 @@ +package io.openems.edge.meter.hager.ecr380d; + +import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.MeterType; +import io.openems.common.utils.ConfigUtils; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + private String id; + private String modbusId; + private int modbusUnitId; + private MeterType type; + + private Builder() { + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setModbusId(String modbusId) { + this.modbusId = modbusId; + return this; + } + + public Builder setType(MeterType type) { + this.type = type; + 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 modbus_id() { + return this.builder.modbusId; + } + + @Override + public String Modbus_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), this.modbus_id()); + } + + @Override + public int modbusUnitId() { + return this.builder.modbusUnitId; + } + + @Override + public MeterType type() { + return this.builder.type; + } + +} \ No newline at end of file