diff --git a/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/Config.java b/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/Config.java index 8512870c35e..22712118e26 100644 --- a/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/Config.java +++ b/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/Config.java @@ -4,6 +4,7 @@ import org.osgi.service.metatype.annotations.ObjectClassDefinition; import io.openems.backend.metadata.odoo.odoo.Protocol; +import io.openems.common.types.DebugMode; @ObjectClassDefinition(// name = "Metadata.Odoo", // diff --git a/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/MetadataOdoo.java b/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/MetadataOdoo.java index 9f28bc32346..c4e0d1c1549 100644 --- a/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/MetadataOdoo.java +++ b/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/MetadataOdoo.java @@ -71,6 +71,7 @@ import io.openems.common.oem.OpenemsBackendOem; import io.openems.common.session.Language; import io.openems.common.session.Role; +import io.openems.common.types.DebugMode; import io.openems.common.types.EdgeConfig; import io.openems.common.types.EdgeConfigDiff; import io.openems.common.types.SemanticVersion; diff --git a/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java b/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java index f5327adf50d..93c6e17ef6e 100644 --- a/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java +++ b/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java @@ -78,6 +78,8 @@ public SystemUpdateParams getSystemUpdateParams() { .put("App.TimeOfUseTariff.Tibber", "") // .put("App.Api.ModbusTcp.ReadOnly", "") // .put("App.Api.ModbusTcp.ReadWrite", "") // + .put("App.Api.ModbusRtu.ReadOnly", "") // + .put("App.Api.ModbusRtu.ReadWrite", "") // .put("App.Api.RestJson.ReadOnly", "") // .put("App.Api.RestJson.ReadWrite", "") // .put("App.Timedata.InfluxDb", "")// diff --git a/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/DebugMode.java b/io.openems.common/src/io/openems/common/types/DebugMode.java similarity index 88% rename from io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/DebugMode.java rename to io.openems.common/src/io/openems/common/types/DebugMode.java index 66c334c44c9..a2b1469fbb8 100644 --- a/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/DebugMode.java +++ b/io.openems.common/src/io/openems/common/types/DebugMode.java @@ -1,4 +1,4 @@ -package io.openems.backend.metadata.odoo; +package io.openems.common.types; public enum DebugMode { diff --git a/io.openems.common/src/io/openems/common/utils/JsonUtils.java b/io.openems.common/src/io/openems/common/utils/JsonUtils.java index 954e55fa99d..68329dbc164 100644 --- a/io.openems.common/src/io/openems/common/utils/JsonUtils.java +++ b/io.openems.common/src/io/openems/common/utils/JsonUtils.java @@ -23,7 +23,6 @@ import com.google.common.base.Function; import com.google.common.base.Supplier; -import com.google.common.collect.Sets; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -445,7 +444,7 @@ public static class JsonArrayCollector implements Collector characteristics() { - return Sets.newHashSet().stream().collect(Sets.toImmutableEnumSet()); + return Set.of(); } @Override @@ -461,7 +460,7 @@ public BiConsumer accumulator() { @Override public BinaryOperator combiner() { return (t, u) -> { - u.build().forEach(j -> t.add(j)); + u.build().forEach(t::add); return t; }; } @@ -604,7 +603,6 @@ public static JsonPrimitive getAsPrimitive(JsonElement jElement, String memberNa * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link JsonPrimitive} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalPrimitive(JsonElement jElement, String memberName) { return Optional.ofNullable(toPrimitive(toSubElement(jElement, memberName))); @@ -634,7 +632,6 @@ public static JsonElement getSubElement(JsonElement jElement, String memberName) * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link JsonElement} value - * @throws OpenemsNamedException on error */ public static Optional getOptionalSubElement(JsonElement jElement, String memberName) { return Optional.ofNullable(toSubElement(jElement, memberName)); @@ -677,7 +674,6 @@ public static JsonObject getAsJsonObject(JsonElement jElement, String memberName * * @param jElement the {@link JsonElement} * @return the {@link Optional} {@link JsonObject} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalJsonObject(JsonElement jElement) { return Optional.ofNullable(toJsonObject(jElement)); @@ -690,7 +686,6 @@ public static Optional getAsOptionalJsonObject(JsonElement jElement) * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link JsonObject} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalJsonObject(JsonElement jElement, String memberName) { return Optional.ofNullable(toJsonObject(toSubElement(jElement, memberName))); @@ -733,7 +728,6 @@ public static JsonArray getAsJsonArray(JsonElement jElement, String memberName) * * @param jElement the {@link JsonElement} * @return the {@link Optional} {@link JsonArray} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalJsonArray(JsonElement jElement) { return Optional.ofNullable(toJsonArray(jElement)); @@ -746,7 +740,6 @@ public static Optional getAsOptionalJsonArray(JsonElement jElement) { * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link JsonArray} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalJsonArray(JsonElement jElement, String memberName) { return Optional.ofNullable(toJsonArray(toSubElement(jElement, memberName))); @@ -789,7 +782,6 @@ public static String getAsString(JsonElement jElement, String memberName) throws * * @param jElement the {@link JsonElement} * @return the {@link Optional} {@link String} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalString(JsonElement jElement) { return Optional.ofNullable(toString(toPrimitive(jElement))); @@ -802,7 +794,6 @@ public static Optional getAsOptionalString(JsonElement jElement) { * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link String} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalString(JsonElement jElement, String memberName) { return Optional.ofNullable(toString(toPrimitive(toSubElement(jElement, memberName)))); @@ -851,7 +842,7 @@ public static String[] getAsStringArray(JsonArray json) throws OpenemsNamedExcep public static boolean getAsBoolean(JsonElement jElement) throws OpenemsNamedException { var value = toBoolean(toPrimitive(jElement)); if (value != null) { - return value.booleanValue(); + return value; } throw OpenemsError.JSON_NO_BOOLEAN.exception(jElement.toString().replace("%", "%%")); } @@ -867,7 +858,7 @@ public static boolean getAsBoolean(JsonElement jElement) throws OpenemsNamedExce public static boolean getAsBoolean(JsonElement jElement, String memberName) throws OpenemsNamedException { var value = toBoolean(toPrimitive(toSubElement(jElement, memberName))); if (value != null) { - return value.booleanValue(); + return value; } throw OpenemsError.JSON_NO_BOOLEAN_MEMBER.exception(memberName, jElement.toString().replace("%", "%%")); } @@ -877,7 +868,6 @@ public static boolean getAsBoolean(JsonElement jElement, String memberName) thro * * @param jElement the {@link JsonElement} * @return the {@link Optional} {@link Boolean} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalBoolean(JsonElement jElement) { return Optional.ofNullable(toBoolean(toPrimitive(jElement))); @@ -890,7 +880,6 @@ public static Optional getAsOptionalBoolean(JsonElement jElement) { * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link Boolean} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalBoolean(JsonElement jElement, String memberName) { return Optional.ofNullable(toBoolean(toPrimitive(toSubElement(jElement, memberName)))); @@ -906,7 +895,7 @@ public static Optional getAsOptionalBoolean(JsonElement jElement, Strin public static short getAsShort(JsonElement jElement) throws OpenemsNamedException { var value = toShort(toPrimitive(jElement)); if (value != null) { - return value.shortValue(); + return value; } throw OpenemsError.JSON_NO_SHORT.exception(jElement.toString().replace("%", "%%")); } @@ -932,7 +921,6 @@ public static short getAsShort(JsonElement jElement, String memberName) throws O * * @param jElement the {@link JsonElement} * @return the {@link Optional} {@link Short} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalShort(JsonElement jElement) { return Optional.ofNullable(toShort(toPrimitive(jElement))); @@ -945,7 +933,6 @@ public static Optional getAsOptionalShort(JsonElement jElement) { * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link Boolean} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalShort(JsonElement jElement, String memberName) { return Optional.ofNullable(toShort(toPrimitive(toSubElement(jElement, memberName)))); @@ -961,7 +948,7 @@ public static Optional getAsOptionalShort(JsonElement jElement, String me public static int getAsInt(JsonElement jElement) throws OpenemsNamedException { var value = toInt(toPrimitive(jElement)); if (value != null) { - return value.intValue(); + return value; } throw OpenemsError.JSON_NO_INTEGER.exception(jElement.toString().replace("%", "%%")); } @@ -1002,7 +989,6 @@ public static int getAsInt(JsonArray jArray, int index) throws OpenemsNamedExcep * * @param jElement the {@link JsonElement} * @return the {@link Optional} {@link Integer} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalInt(JsonElement jElement) { return Optional.ofNullable(toInt(toPrimitive(jElement))); @@ -1015,7 +1001,6 @@ public static Optional getAsOptionalInt(JsonElement jElement) { * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link Integer} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalInt(JsonElement jElement, String memberName) { return Optional.ofNullable(toInt(toPrimitive(toSubElement(jElement, memberName)))); @@ -1031,7 +1016,7 @@ public static Optional getAsOptionalInt(JsonElement jElement, String me public static long getAsLong(JsonElement jElement) throws OpenemsNamedException { var value = toLong(toPrimitive(jElement)); if (value != null) { - return value.longValue(); + return value; } throw OpenemsError.JSON_NO_LONG.exception(jElement.toString().replace("%", "%%")); } @@ -1057,7 +1042,6 @@ public static long getAsLong(JsonElement jElement, String memberName) throws Ope * * @param jElement the {@link JsonElement} * @return the {@link Optional} {@link Long} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalLong(JsonElement jElement) { return Optional.ofNullable(toLong(toPrimitive(jElement))); @@ -1069,7 +1053,6 @@ public static Optional getAsOptionalLong(JsonElement jElement) { * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link Long} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalLong(JsonElement jElement, String memberName) { return Optional.ofNullable(toLong(toPrimitive(toSubElement(jElement, memberName)))); @@ -1085,7 +1068,7 @@ public static Optional getAsOptionalLong(JsonElement jElement, String memb public static float getAsFloat(JsonElement jElement) throws OpenemsNamedException { var value = toFloat(toPrimitive(jElement)); if (value != null) { - return value.floatValue(); + return value; } throw OpenemsError.JSON_NO_FLOAT.exception(jElement.toString().replace("%", "%%")); } @@ -1111,7 +1094,6 @@ public static float getAsFloat(JsonElement jElement, String memberName) throws O * * @param jElement the {@link JsonElement} * @return the {@link Optional} {@link Float} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalFloat(JsonElement jElement) { return Optional.ofNullable(toFloat(toPrimitive(jElement))); @@ -1123,7 +1105,6 @@ public static Optional getAsOptionalFloat(JsonElement jElement) { * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link Float} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalFloat(JsonElement jElement, String memberName) { return Optional.ofNullable(toFloat(toPrimitive(toSubElement(jElement, memberName)))); @@ -1139,7 +1120,7 @@ public static Optional getAsOptionalFloat(JsonElement jElement, String me public static double getAsDouble(JsonElement jElement) throws OpenemsNamedException { var value = toDouble(toPrimitive(jElement)); if (value != null) { - return value.doubleValue(); + return value; } throw OpenemsError.JSON_NO_DOUBLE.exception(jElement.toString().replace("%", "%%")); } @@ -1165,7 +1146,6 @@ public static double getAsDouble(JsonElement jElement, String memberName) throws * * @param jElement the {@link JsonElement} * @return the {@link Optional} {@link Double} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalDouble(JsonElement jElement) { return Optional.ofNullable(toDouble(toPrimitive(jElement))); @@ -1178,7 +1158,6 @@ public static Optional getAsOptionalDouble(JsonElement jElement) { * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link Double} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalDouble(JsonElement jElement, String memberName) { return Optional.ofNullable(toDouble(toPrimitive(toSubElement(jElement, memberName)))); @@ -1228,7 +1207,6 @@ public static > E getAsEnum(Class enumType, JsonElement jEl * @param enumType the class of the {@link Enum} * @param jElement the {@link JsonElement} * @return the {@link Optional} {@link Enum} value - * @throws OpenemsNamedException on error */ public static > Optional getAsOptionalEnum(Class enumType, JsonElement jElement) { return Optional.ofNullable(toEnum(enumType, toString(toPrimitive(jElement)))); @@ -1242,7 +1220,6 @@ public static > Optional getAsOptionalEnum(Class enumTyp * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link Enum} value - * @throws OpenemsNamedException on error */ public static > Optional getAsOptionalEnum(Class enumType, JsonElement jElement, String memberName) { @@ -1285,7 +1262,6 @@ public static Inet4Address getAsInet4Address(JsonElement jElement, String member * * @param jElement the {@link JsonElement} * @return the {@link Optional} {@link Inet4Address} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalInet4Address(JsonElement jElement) { return Optional.ofNullable(InetAddressUtils.parseOrNull(toString(toPrimitive(jElement)))); @@ -1298,7 +1274,6 @@ public static Optional getAsOptionalInet4Address(JsonElement jElem * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link Inet4Address} value - * @throws OpenemsNamedException on error */ public static Optional getAsOptionalInet4Address(JsonElement jElement, String memberName) { return Optional.ofNullable(// @@ -1345,7 +1320,6 @@ public static UUID getAsUUID(JsonElement jElement, String memberName) throws Ope * * @param jElement the {@link JsonElement} * @return the {@link Optional} {@link UUID} value - * @throws OpenemsNamedException on error */ // CHECKSTYLE:OFF public static Optional getAsOptionalUUID(JsonElement jElement) { @@ -1359,7 +1333,6 @@ public static Optional getAsOptionalUUID(JsonElement jElement) { * @param jElement the {@link JsonElement} * @param memberName the name of the member * @return the {@link Optional} {@link UUID} value - * @throws OpenemsNamedException on error */ // CHECKSTYLE:OFF public static Optional getAsOptionalUUID(JsonElement jElement, String memberName) { @@ -1379,7 +1352,7 @@ public static Object getAsBestType(JsonElement j) throws OpenemsNamedException { try { if (j.isJsonArray()) { var jA = (JsonArray) j; - if (jA.size() == 0) { + if (jA.isEmpty()) { return new Object[0]; } // identify the array type (boolean, int or String) @@ -1453,132 +1426,88 @@ public static Object getAsBestType(JsonElement j) throws OpenemsNamedException { * @return the {@link JsonElement} */ public static JsonElement getAsJsonElement(Object value) { - // null - if (value == null) { - return JsonNull.INSTANCE; - } // optional - if (value instanceof Optional) { - if (!((Optional) value).isPresent()) { + if (value instanceof Optional opt) { + if (opt.isEmpty()) { return JsonNull.INSTANCE; } - value = ((Optional) value).get(); - } - if (value instanceof Number) { - /* - * Number - */ - return new JsonPrimitive((Number) value); - } - if (value instanceof String) { - /* - * String - */ - return new JsonPrimitive((String) value); - } - if (value instanceof Boolean) { - /* - * Boolean - */ - return new JsonPrimitive((Boolean) value); - } - if (value instanceof Inet4Address) { - /* - * Inet4Address - */ - return new JsonPrimitive(((Inet4Address) value).getHostAddress()); - } - if (value instanceof JsonElement) { - /* - * JsonElement - */ - return (JsonElement) value; - } else if (value instanceof boolean[]) { - /* - * boolean-Array - */ + value = opt.get(); + } + + return switch (value) { + case null -> JsonNull.INSTANCE; + case Number n -> new JsonPrimitive(n); + case String s -> new JsonPrimitive(s); + case Boolean b -> new JsonPrimitive(b); + case Inet4Address inet -> new JsonPrimitive(inet.getHostAddress()); + case JsonElement json -> json; + case boolean[] bool -> { var js = new JsonArray(); - for (boolean b : (boolean[]) value) { + for (boolean b : bool) { js.add(new JsonPrimitive(b)); } - return js; - } else if (value instanceof short[]) { - /* - * short-Array - */ + yield js; + } + case short[] shorts -> { var js = new JsonArray(); - for (short s : (short[]) value) { + for (short s : shorts) { js.add(new JsonPrimitive(s)); } - return js; - } else if (value instanceof int[]) { - /* - * int-Array - */ + yield js; + } + case int[] ints -> { var js = new JsonArray(); - for (int i : (int[]) value) { + for (int i : ints) { js.add(new JsonPrimitive(i)); } - return js; - } else if (value instanceof long[]) { - /* - * long-Array - */ + yield js; + } + case long[] longs -> { var js = new JsonArray(); - for (long l : (long[]) value) { + for (long l : longs) { js.add(new JsonPrimitive(l)); } - return js; - } else if (value instanceof float[]) { - /* - * float-Array - */ + yield js; + } + case float[] floats -> { var js = new JsonArray(); - for (float f : (float[]) value) { + for (float f : floats) { js.add(new JsonPrimitive(f)); } - return js; - } else if (value instanceof double[]) { - /* - * double-Array - */ + yield js; + } + case double[] doubles -> { var js = new JsonArray(); - for (double d : (double[]) value) { - js.add(new JsonPrimitive(d)); + for (double f : doubles) { + js.add(new JsonPrimitive(f)); } - return js; - } else if (value instanceof String[]) { - /* - * String-Array - */ + yield js; + } + case String[] strings -> { var js = new JsonArray(); - var v = (String[]) value; - if (v.length == 1 && v[0].isEmpty()) { + if (strings.length == 1 && strings[0].isEmpty()) { // special case: String-Array with one entry which is an empty String. Return an // empty JsonArray. - return js; + yield js; } - for (String s : v) { + for (String s : strings) { js.add(new JsonPrimitive(s)); } - return js; - } else if (value instanceof Object[]) { - /* - * Object-Array - */ + yield js; + } + case Object[] objects -> { var js = new JsonArray(); - for (Object o : (Object[]) value) { + for (Object o : objects) { js.add(JsonUtils.getAsJsonElement(o)); } - return js; - } else { - /* - * Use toString()-method - */ - JsonUtils.LOG.warn("Converter for [" + value + "]" + " of type [" + value.getClass().getSimpleName() - + "] to JSON is not implemented."); - return new JsonPrimitive(value.toString()); + yield js; } + default -> { + JsonUtils.LOG.warn("Converter for [{}] of type [{}] to JSON is not implemented.", // + value, value.getClass().getSimpleName()); + yield new JsonPrimitive(value.toString()); + } + }; } /** @@ -1631,11 +1560,11 @@ public static Object getAsType(Class type, JsonElement j) throws NotImplement */ return j.getAsJsonArray(); } else if (type.isArray()) { - /** + /* * Asking for Array */ if (Long.class.isAssignableFrom(type.getComponentType())) { - /** + /* * Asking for ArrayOfLong */ if (j.isJsonArray()) { @@ -1671,22 +1600,15 @@ public static T getAsType(OpenemsType type, JsonElement j) throws OpenemsNam } if (j.isJsonPrimitive()) { - switch (type) { - case BOOLEAN: - return (T) Boolean.valueOf(JsonUtils.getAsBoolean(j)); - case DOUBLE: - return (T) Double.valueOf(JsonUtils.getAsDouble(j)); - case FLOAT: - return (T) Float.valueOf(JsonUtils.getAsFloat(j)); - case INTEGER: - return (T) Integer.valueOf(JsonUtils.getAsInt(j)); - case LONG: - return (T) Long.valueOf(JsonUtils.getAsLong(j)); - case SHORT: - return (T) Short.valueOf(JsonUtils.getAsShort(j)); - case STRING: - return (T) JsonUtils.getAsString(j); - } + return switch (type) { + case BOOLEAN -> (T) Boolean.valueOf(JsonUtils.getAsBoolean(j)); + case DOUBLE -> (T) Double.valueOf(JsonUtils.getAsDouble(j)); + case FLOAT -> (T) Float.valueOf(JsonUtils.getAsFloat(j)); + case INTEGER -> (T) Integer.valueOf(JsonUtils.getAsInt(j)); + case LONG -> (T) Long.valueOf(JsonUtils.getAsLong(j)); + case SHORT -> (T) Short.valueOf(JsonUtils.getAsShort(j)); + case STRING -> (T) JsonUtils.getAsString(j); + }; } if (j.isJsonObject() || j.isJsonArray()) { @@ -1715,7 +1637,7 @@ public static T getAsType(OpenemsType type, JsonElement j) throws OpenemsNam * @return an Object of the given type */ public static Object getAsType(Optional> typeOptional, JsonElement j) throws NotImplementedException { - if (!typeOptional.isPresent()) { + if (typeOptional.isEmpty()) { throw new NotImplementedException( "Type of Channel was not set: " + (j == null ? "UNDEFINED" : j.getAsString())); } @@ -1880,7 +1802,7 @@ public static String prettyToString(JsonElement j) { public static boolean isEmptyJsonObject(JsonElement j) { if (j != null && j.isJsonObject()) { var object = j.getAsJsonObject(); - return object.size() == 0; + return object.isEmpty(); } return false; @@ -1895,7 +1817,7 @@ public static boolean isEmptyJsonObject(JsonElement j) { public static boolean isEmptyJsonArray(JsonElement j) { if (j != null && j.isJsonArray()) { var array = j.getAsJsonArray(); - return array.size() == 0; + return array.isEmpty(); } return false; @@ -1920,7 +1842,7 @@ public static boolean isNumber(JsonElement j) { */ public static Stream stream(JsonArray jsonArray) { return IntStream.range(0, jsonArray.size()) // - .mapToObj(index -> jsonArray.get(index)); + .mapToObj(jsonArray::get); } private static JsonObject toJsonObject(JsonElement jElement) { diff --git a/io.openems.common/src/io/openems/common/utils/StreamUtils.java b/io.openems.common/src/io/openems/common/utils/StreamUtils.java new file mode 100644 index 00000000000..9a831452028 --- /dev/null +++ b/io.openems.common/src/io/openems/common/utils/StreamUtils.java @@ -0,0 +1,25 @@ +package io.openems.common.utils; + +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Stream; + +public class StreamUtils { + + /** + * Converts a Dictionary to a Stream of Map entries. + * + * @param dictionary the Dictionary to be converted + * @param the type of keys in the Dictionary + * @param the type of values in the Dictionary + * @return a Stream containing all the key-value pairs from the Dictionary as + * Map entries + */ + public static Stream> dictionaryToStream(Dictionary dictionary) { + Enumeration keys = dictionary.keys(); + return Collections.list(keys).stream().map(key -> Map.entry(key, dictionary.get(key))); + } +} diff --git a/io.openems.edge.batteryinverter.api/src/io/openems/edge/batteryinverter/api/SymmetricBatteryInverter.java b/io.openems.edge.batteryinverter.api/src/io/openems/edge/batteryinverter/api/SymmetricBatteryInverter.java index 32aa035b4f1..8e731a87612 100644 --- a/io.openems.edge.batteryinverter.api/src/io/openems/edge/batteryinverter/api/SymmetricBatteryInverter.java +++ b/io.openems.edge.batteryinverter.api/src/io/openems/edge/batteryinverter/api/SymmetricBatteryInverter.java @@ -135,6 +135,20 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { DC_MAX_VOLTAGE(Doc.of(OpenemsType.INTEGER) // .unit(Unit.VOLT) // .persistencePriority(PersistencePriority.HIGH) // + ), // + + /** + * Inverter Cabinet Temperature. + * + *
    + *
  • Interface: SymmetricBatteryInverter + *
  • Type: Integer + *
  • Unit: C + *
+ */ + TEMPERATURE_CABINET(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.DEGREE_CELSIUS) // + .persistencePriority(PersistencePriority.HIGH) // ); private final Doc doc; @@ -463,4 +477,23 @@ public default void _setDcMaxVoltage(Integer value) { public default void _setDcMaxVoltage(int value) { this.getDcMaxVoltageChannel().setNextValue(value); } + + /** + * Gets the Channel for {@link ChannelId#TEMPERATURE_CABINET}. + * + * @return the Channel + */ + public default IntegerReadChannel getTemperatureCabinetChannel() { + return this.channel(ChannelId.TEMPERATURE_CABINET); + } + + /** + * Gets the Inverters Cabinet temperature in [C]. See + * {@link ChannelId#TEMPERATURE_CABINET}. + * + * @return the Channel {@link Value} + */ + public default Value getTemperatureCabinet() { + return this.getTemperatureCabinetChannel().value(); + } } diff --git a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/BridgeHttpImpl.java b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/BridgeHttpImpl.java index 460236c183c..4cf1838b0a2 100644 --- a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/BridgeHttpImpl.java +++ b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/BridgeHttpImpl.java @@ -20,6 +20,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.openems.common.types.DebugMode; import io.openems.common.utils.FunctionUtils; import io.openems.edge.bridge.http.api.BridgeHttp; import io.openems.edge.bridge.http.api.BridgeHttpExecutor; @@ -145,6 +146,8 @@ public void shutdown() { private final Set timeEndpoints = ConcurrentHashMap.newKeySet(); + private DebugMode debugMode = DebugMode.OFF; + @Activate public BridgeHttpImpl(// @Reference final CycleSubscriber cycleSubscriber, // @@ -170,6 +173,11 @@ public void deactivate() { this.timeEndpoints.clear(); } + @Override + public void setDebugMode(DebugMode debugMode) { + this.debugMode = debugMode; + } + @Override public CycleEndpoint subscribeCycle(CycleEndpoint endpoint) { Objects.requireNonNull(endpoint, "CycleEndpoint is not allowed to be null!"); @@ -202,7 +210,7 @@ public CompletableFuture> request(Endpoint endpoint) { final var future = new CompletableFuture>(); this.pool.execute(() -> { try { - final var result = this.urlFetcher.fetchEndpoint(endpoint); + final var result = this.urlFetcher.fetchEndpoint(endpoint, this.debugMode); future.complete(result); } catch (HttpError e) { future.completeExceptionally(e); @@ -252,7 +260,8 @@ private void handleEvent(Event event) { private Runnable createTask(CycleEndpointCountdown endpointItem) { return () -> { try { - final var result = this.urlFetcher.fetchEndpoint(endpointItem.getCycleEndpoint().endpoint().get()); + final var result = this.urlFetcher.fetchEndpoint(endpointItem.getCycleEndpoint().endpoint().get(), + this.debugMode); endpointItem.getCycleEndpoint().onResult().accept(result); } catch (HttpError e) { endpointItem.getCycleEndpoint().onError().accept(e); @@ -275,7 +284,8 @@ private Runnable createTask(TimeEndpointCountdown endpointCountdown) { HttpResponse result = null; HttpError error = null; try { - result = this.urlFetcher.fetchEndpoint(endpointCountdown.getTimeEndpoint().endpoint().get()); + result = this.urlFetcher.fetchEndpoint(endpointCountdown.getTimeEndpoint().endpoint().get(), + this.debugMode); endpointCountdown.getTimeEndpoint().onResult().accept(result); } catch (HttpError e) { endpointCountdown.getTimeEndpoint().onError().accept(e); diff --git a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/NetworkEndpointFetcher.java b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/NetworkEndpointFetcher.java index 3a751cd7ffc..9add30c9497 100644 --- a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/NetworkEndpointFetcher.java +++ b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/NetworkEndpointFetcher.java @@ -10,9 +10,13 @@ import java.net.URI; import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import io.openems.common.types.DebugMode; import io.openems.common.types.HttpStatus; import io.openems.edge.bridge.http.api.BridgeHttp.Endpoint; +import io.openems.edge.bridge.http.dummy.DummyEndpointFetcher; import io.openems.edge.bridge.http.api.EndpointFetcher; import io.openems.edge.bridge.http.api.HttpError; import io.openems.edge.bridge.http.api.HttpResponse; @@ -20,8 +24,10 @@ @Component public class NetworkEndpointFetcher implements EndpointFetcher { + private final Logger log = LoggerFactory.getLogger(DummyEndpointFetcher.class); + @Override - public HttpResponse fetchEndpoint(final Endpoint endpoint) throws HttpError { + public HttpResponse fetchEndpoint(final Endpoint endpoint, DebugMode mode) throws HttpError { try { var url = URI.create(endpoint.url()).toURL(); var con = (HttpURLConnection) url.openConnection(); @@ -53,6 +59,12 @@ public HttpResponse fetchEndpoint(final Endpoint endpoint) throws HttpEr if (status.isError()) { throw new HttpError.ResponseError(status, body); } + if (mode.equals(DebugMode.DETAILED)) { + this.log.debug("Fetched Endpoint for request: " + endpoint.url() + "\n" // + + "method: " + endpoint.method().name() + "\n" // + + "result: " + body // + ); + } return new HttpResponse<>(status, body); } catch (IOException e) { throw new HttpError.UnknownError(e); diff --git a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/BridgeHttp.java b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/BridgeHttp.java index 52e198f75f8..d3cbf4352e9 100644 --- a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/BridgeHttp.java +++ b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/BridgeHttp.java @@ -10,6 +10,7 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.function.ThrowingFunction; +import io.openems.common.types.DebugMode; import io.openems.common.utils.JsonUtils; /** @@ -73,6 +74,8 @@ public record Endpoint(// } + public void setDebugMode(DebugMode debugMode); + /** * Fetches the url once with {@link HttpMethod#GET}. * diff --git a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/EndpointFetcher.java b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/EndpointFetcher.java index c2467110749..e0d1bb54235 100644 --- a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/EndpointFetcher.java +++ b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/EndpointFetcher.java @@ -1,6 +1,7 @@ package io.openems.edge.bridge.http.api; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.DebugMode; import io.openems.edge.bridge.http.api.BridgeHttp.Endpoint; public interface EndpointFetcher { @@ -9,10 +10,11 @@ public interface EndpointFetcher { * Creates a {@link Runnable} to execute a request with the given parameters. * * @param endpoint the {@link Endpoint} to fetch + * @param mode the {@link DebugMode} * * @return the result of the {@link Endpoint} * @throws OpenemsNamedException on error */ - public HttpResponse fetchEndpoint(Endpoint endpoint) throws HttpError; + public HttpResponse fetchEndpoint(Endpoint endpoint, DebugMode mode) throws HttpError; } diff --git a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttp.java b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttp.java index 99040297ca2..647c1d3e6be 100644 --- a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttp.java +++ b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyBridgeHttp.java @@ -6,6 +6,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; +import io.openems.common.types.DebugMode; import io.openems.edge.bridge.http.api.BridgeHttp; import io.openems.edge.bridge.http.api.HttpResponse; @@ -55,4 +56,9 @@ public Collection removeTimeEndpointIf(Predicate con return emptyList(); } + @Override + public void setDebugMode(DebugMode debugMode) { + // do nothing + } + } diff --git a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyEndpointFetcher.java b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyEndpointFetcher.java index 907c714f0d5..269148dd1a9 100644 --- a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyEndpointFetcher.java +++ b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/dummy/DummyEndpointFetcher.java @@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory; import io.openems.common.function.ThrowingFunction; +import io.openems.common.types.DebugMode; import io.openems.common.utils.FunctionUtils; import io.openems.edge.bridge.http.api.BridgeHttp.Endpoint; import io.openems.edge.bridge.http.api.EndpointFetcher; @@ -29,7 +30,8 @@ public record DummyHandler(// @Override public HttpResponse fetchEndpoint(// - final Endpoint endpoint // + final Endpoint endpoint, // + DebugMode mode // ) throws HttpError { try { for (final var iterator = this.urlHandler.iterator(); iterator.hasNext();) { @@ -89,5 +91,4 @@ public void addSingleUseEndpointHandler(ThrowingFunction getComponentProperties(String componentId); + /** * Gets all enabled OpenEMS-Components. * diff --git a/io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java b/io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java index b065cb50c18..c42c686cdc2 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java +++ b/io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java @@ -398,7 +398,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ CONSUMPTION_ACTIVE_POWER(Doc.of(OpenemsType.INTEGER) // .unit(Unit.WATT) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Active power of the electrical consumption")), // /** * Consumption: Active Power L1. * @@ -413,7 +414,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ CONSUMPTION_ACTIVE_POWER_L1(Doc.of(OpenemsType.INTEGER) // .unit(Unit.WATT) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Active power of the electrical consumption on phase L1")), // /** * Consumption: Active Power L2. * @@ -428,7 +430,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ CONSUMPTION_ACTIVE_POWER_L2(Doc.of(OpenemsType.INTEGER) // .unit(Unit.WATT) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Active power of the electrical consumption on phase L2")), // /** * Consumption: Active Power L3. * @@ -443,7 +446,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ CONSUMPTION_ACTIVE_POWER_L3(Doc.of(OpenemsType.INTEGER) // .unit(Unit.WATT) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Active power of the electrical consumption on phase L3")), // /** * Consumption: Maximum Ever Active Power. * @@ -456,7 +460,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ CONSUMPTION_MAX_ACTIVE_POWER(Doc.of(OpenemsType.INTEGER) // .unit(Unit.WATT) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Maximum measured active power of the electrical consumpton")), // /** * Unmanaged Consumption: Active Power. * @@ -521,7 +526,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ ESS_ACTIVE_CHARGE_ENERGY(Doc.of(OpenemsType.LONG) // .unit(Unit.CUMULATED_WATT_HOURS) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Accumulated electrical energy of the AC-side storage charging incl. excess PV generation at the hybrid inverter")), // /** * Ess: Active Discharge Energy. * @@ -533,7 +539,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ ESS_ACTIVE_DISCHARGE_ENERGY(Doc.of(OpenemsType.LONG) // .unit(Unit.CUMULATED_WATT_HOURS) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Accumulated electrical energy of the AC-side storage discharge incl. excess PV generation at the hybrid inverter")), // /** * Ess: DC Discharge Energy. * @@ -545,7 +552,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ ESS_DC_DISCHARGE_ENERGY(Doc.of(OpenemsType.LONG) // .unit(Unit.CUMULATED_WATT_HOURS) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Accumulated DC electrical energy of the storage discharging")), // /** * Ess: DC Charge Energy. * @@ -557,7 +565,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ ESS_DC_CHARGE_ENERGY(Doc.of(OpenemsType.LONG) // .unit(Unit.CUMULATED_WATT_HOURS) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Accumulated DC electrical energy of the storage charging")), // /** * Grid: Buy-from-grid Energy ("Production"). * @@ -569,7 +578,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ GRID_BUY_ACTIVE_ENERGY(Doc.of(OpenemsType.LONG) // .unit(Unit.CUMULATED_WATT_HOURS) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Accumulated electrical energy of grid consumption")), // /** * Grid: Sell-to-grid Energy ("Consumption"). * @@ -581,7 +591,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ GRID_SELL_ACTIVE_ENERGY(Doc.of(OpenemsType.LONG) // .unit(Unit.CUMULATED_WATT_HOURS) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Accumulated electrical energy of grid feed-in")), // /** * Production: Energy. * @@ -592,7 +603,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ PRODUCTION_ACTIVE_ENERGY(Doc.of(OpenemsType.LONG) // .unit(Unit.CUMULATED_WATT_HOURS) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Accumulated electrical energy of DC- and AC-side generators, e.g. photovoltaics")), // /** * Production: AC Energy. * @@ -604,7 +616,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ PRODUCTION_AC_ACTIVE_ENERGY(Doc.of(OpenemsType.LONG) // .unit(Unit.CUMULATED_WATT_HOURS) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Accumulated electrical energy of AC-side generators")), // /** * Production: DC Energy. * @@ -616,7 +629,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ PRODUCTION_DC_ACTIVE_ENERGY(Doc.of(OpenemsType.LONG) // .unit(Unit.CUMULATED_WATT_HOURS) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Accumulated electrical energy of DC-side generators")), // /** * Consumption: Energy. * @@ -628,7 +642,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ CONSUMPTION_ACTIVE_ENERGY(Doc.of(OpenemsType.LONG) // .unit(Unit.CUMULATED_WATT_HOURS) // - .persistencePriority(PersistencePriority.VERY_HIGH)), // + .persistencePriority(PersistencePriority.VERY_HIGH) // + .text("Accumulated electrical energy consumption")), // /** * Is there any Component Info/Warning/Fault that is getting ignored/hidden diff --git a/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentManager.java b/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentManager.java index fe893c48292..45bf636acd1 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentManager.java +++ b/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentManager.java @@ -7,8 +7,12 @@ import java.time.Clock; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Hashtable; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; import org.osgi.framework.InvalidSyntaxException; import org.osgi.service.cm.ConfigurationAdmin; @@ -29,6 +33,7 @@ import io.openems.common.jsonrpc.response.GetEdgeConfigResponse; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.JsonUtils; +import io.openems.common.utils.StreamUtils; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.component.OpenemsComponent; @@ -271,4 +276,15 @@ public void setConfigurationAdmin(ConfigurationAdmin configurationAdmin) { this.configurationAdmin = configurationAdmin; } + @Override + public Map getComponentProperties(String componentId) { + try { + var component = this.getComponent(componentId); + return StreamUtils.dictionaryToStream(component.getComponentContext().getProperties())// + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + } catch (OpenemsNamedException e) { + return Collections.emptyMap(); + } + } + } \ No newline at end of file diff --git a/io.openems.edge.controller.api.modbus/bnd.bnd b/io.openems.edge.controller.api.modbus/bnd.bnd index 87fd5d5a4a8..aaaa2848a5c 100644 --- a/io.openems.edge.controller.api.modbus/bnd.bnd +++ b/io.openems.edge.controller.api.modbus/bnd.bnd @@ -5,14 +5,15 @@ Bundle-Version: 1.0.0.${tstamp} -buildpath: \ ${buildpath},\ - com.ghgande.j2mod;version=2.5.5,\ + com.ghgande.j2mod,\ io.openems.common,\ + io.openems.edge.bridge.modbus,\ io.openems.edge.common,\ io.openems.edge.controller.api,\ io.openems.edge.controller.api.common,\ io.openems.edge.ess.api,\ io.openems.edge.timedata.api,\ - io.openems.wrapper.fastexcel + io.openems.wrapper.fastexcel,\ -testpath: \ ${testpath} diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusApi.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusApi.java new file mode 100644 index 00000000000..6ffd1472af7 --- /dev/null +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusApi.java @@ -0,0 +1,484 @@ +package io.openems.edge.controller.api.modbus; + +import java.util.List; +import java.util.TreeMap; +import java.util.Map.Entry; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ghgande.j2mod.modbus.ModbusException; +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; + +import io.openems.common.channel.AccessMode; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.utils.ConfigUtils; +import io.openems.common.utils.FunctionUtils; +import io.openems.common.worker.AbstractWorker; +import io.openems.edge.common.channel.Channel; +import io.openems.edge.common.channel.WriteChannel; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.jsonapi.ComponentJsonApi; +import io.openems.edge.common.jsonapi.JsonApiBuilder; +import io.openems.edge.common.jsonapi.JsonrpcEndpointGuard; +import io.openems.edge.common.meta.Meta; +import io.openems.edge.common.modbusslave.ModbusRecord; +import io.openems.edge.common.modbusslave.ModbusRecordChannel; +import io.openems.edge.common.modbusslave.ModbusRecordCycleValue; +import io.openems.edge.common.modbusslave.ModbusRecordString16; +import io.openems.edge.common.modbusslave.ModbusRecordUint16BlockLength; +import io.openems.edge.common.modbusslave.ModbusRecordUint16Hash; +import io.openems.edge.common.modbusslave.ModbusSlave; +import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable; +import io.openems.edge.controller.api.Controller; +import io.openems.edge.controller.api.common.ApiWorker; +import io.openems.edge.controller.api.common.Status; +import io.openems.edge.controller.api.common.WriteObject; +import io.openems.edge.controller.api.common.WritePojo; +import io.openems.edge.controller.api.common.ApiWorker.WriteHandler; +import io.openems.edge.controller.api.modbus.jsonrpc.GetModbusProtocolExportXlsxRequest; +import io.openems.edge.controller.api.modbus.jsonrpc.GetModbusProtocolExportXlsxResponse; +import io.openems.edge.controller.api.modbus.jsonrpc.GetModbusProtocolRequest; +import io.openems.edge.controller.api.modbus.jsonrpc.GetModbusProtocolResponse; + +public abstract class AbstractModbusApi extends AbstractOpenemsComponent + implements ModbusApi, ComponentJsonApi, Controller { + + public static final int UNIT_ID = 1; + public static final int DEFAULT_MAX_CONCURRENT_CONNECTIONS = 5; + + /** + * Holds the link between Modbus start address of a Component and the + * Component-ID. + */ + protected final TreeMap components = new TreeMap<>(); + protected final TreeMap records = new TreeMap<>(); + protected volatile List _components = new CopyOnWriteArrayList<>(); + protected List invalidComponents = new CopyOnWriteArrayList<>(); + protected final Logger log = LoggerFactory.getLogger(AbstractModbusApi.class); + protected final MyProcessImage processImage; + + /** + * Holds the link between Modbus address and ModbusRecord. + */ + protected final ApiWorker apiWorker = new ApiWorker(this, + new WriteHandler(this.handleWrites(), this::setOverrideStatus, this.handleTimeouts())); + + private AbstractModbusConfig config; + + protected AbstractModbusApi(io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds, + io.openems.edge.common.channel.ChannelId[][] furtherInitialChannelIds) { + super(firstInitialChannelIds, furtherInitialChannelIds); + this.processImage = new MyProcessImage(this); + } + + protected void activate(ComponentContext context, ConfigurationAdmin cm, AbstractModbusConfig config) + throws OpenemsException { + this.config = config; + super.activate(context, config.id(), config.alias(), config.enabled()); + + final var filter = ConfigUtils.generateReferenceTargetFilter(this.servicePid(), false, config.componentIds()); + OpenemsComponent.updateReferenceFilterRaw(cm, this.servicePid(), "Component", filter); + + this.apiWorker.setTimeoutSeconds(config.apiTimeout()); + + if (!this.isEnabled()) { + return; + } + + this.startApiWorker.activate(config.id()); + + } + + protected void modified(ComponentContext context, ConfigurationAdmin cm, AbstractModbusConfig config) + throws OpenemsException { + super.modified(context, config.id(), config.alias(), config.enabled()); + + final var filter = ConfigUtils.generateReferenceTargetFilter(this.servicePid(), false, config.componentIds()); + OpenemsComponent.updateReferenceFilterRaw(cm, this.servicePid(), "Component", filter); + + if (this.config.equals(config)) { + return; + } + + this.config = config; + + this.apiWorker.setTimeoutSeconds(config.apiTimeout()); + + if (!this.isEnabled()) { + this.startApiWorker.deactivate(); + return; + } + + this.startApiWorker.modified(config.id()); + + } + + @Override + protected void deactivate() { + this.startApiWorker.deactivate(); + super.deactivate(); + + // wait until modbus slave was completely closed + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + this.log.warn(e.getMessage()); + } + } + + protected void onStarted() { + AbstractModbusApi.this.logInfo(this.log, "ModbusApi started."); + } + + protected Consumer, WriteObject>> handleWrites() { + return FunctionUtils::doNothing; + } + + protected void setOverrideStatus(Status status) { + // do nothing + } + + protected Runnable handleTimeouts() { + return FunctionUtils::doNothing; + } + + protected abstract com.ghgande.j2mod.modbus.slave.ModbusSlave createSlave() throws ModbusException; + + private final AbstractWorker startApiWorker = new AbstractWorker() { + + private static final int DEFAULT_WAIT_TIME = 5000; // 5 seconds + + private final Logger log = LoggerFactory.getLogger(AbstractWorker.class); + + protected com.ghgande.j2mod.modbus.slave.ModbusSlave slave = null; + + protected AbstractModbusConfig currentConfig = null; + + @Override + protected void forever() throws ModbusException { + if (this.slave == null) { + try { + // start new server + this.currentConfig = AbstractModbusApi.this.config; + this.slave = AbstractModbusApi.this.createSlave(); + this.slave.addProcessImage(UNIT_ID, AbstractModbusApi.this.processImage); + this.slave.open(); + if (isEnabled()) { + AbstractModbusApi.this.onStarted(); + AbstractModbusApi.this._setUnableToStart(false); + } + } catch (ModbusException e) { + ModbusSlaveFactory.close(this.slave); + AbstractModbusApi.this.logError(this.log, "Unable to start Modbus-Api: " + e.getMessage()); + AbstractModbusApi.this._setUnableToStart(true); + } + + } else { + // regular check for errors + String error = this.slave.getError(); + if (error != null) { + AbstractModbusApi.this.logError(this.log, "Unable to start Modbus-Api: " + error); + AbstractModbusApi.this._setUnableToStart(true); + this.stopSlave(); + } else if (!this.currentConfig.equals(AbstractModbusApi.this.config)) { + this.stopSlave(); + } + } + } + + private void stopSlave() { + ModbusSlaveFactory.close(this.slave); + this.slave = null; + } + + @Override + protected int getCycleTime() { + return DEFAULT_WAIT_TIME; + } + + }; + + @Override + public void run() throws OpenemsNamedException { + if (!this.isEnabled()) { + return; + } + + this.updateCycleValues(); + this.apiWorker.run(); + } + + /** + * Called by addComponent/removeComponent. Initializes the ModbusRecords, once + * all Components are available. Fault-State otherwise. + */ + protected synchronized void updateComponents() { + var config = this.config; + + if (config == null) { + return; + } + if (config.componentIds().length > this._components.size()) { + if (this.getComponentNoModbusApiFaultChannel().getNextValue().get() != true) { + this._setComponentMissingFault(true); // Either this or that fault + } + return; + } + this._setComponentMissingFault(false); + + this.initializeModbusRecords(this.config.metaComponent(), this.config.componentIds()); + } + + protected synchronized void addComponent(OpenemsComponent component) { + if (!(component instanceof ModbusSlave)) { + this.logError(this.log, "Component [" + component.id() + "] does not implement ModbusSlave"); + this.invalidComponents.add(component); + this._setComponentNoModbusApiFault(true); + return; + } + this._components.add((ModbusSlave) component); + this.updateComponents(); + } + + protected synchronized void removeComponent(OpenemsComponent component) { + this._components.remove(component); + if (this.invalidComponents.remove(component)) { + if (this.invalidComponents.isEmpty()) { + this._setComponentNoModbusApiFault(false); + } + return; + } + this.updateComponents(); + } + + /** + * Once every cycle: update the values for each registered + * {@link ModbusRecordCycleValue}. + */ + @SuppressWarnings("unchecked") + protected void updateCycleValues() { + this.records.values() // + .stream() // + .filter(r -> r instanceof ModbusRecordCycleValue) // + .map(r -> (ModbusRecordCycleValue) r) // + .forEach(r -> { + OpenemsComponent component = this.getPossiblyDisabledComponent(r.getComponentId()); + if (component != null && component.isEnabled()) { + r.updateValue(component); + } else { + r.updateValue(null); + } + }); + } + + @Override + protected void logDebug(Logger log, String message) { + super.logDebug(log, message); + } + + @Override + protected void logInfo(Logger log, String message) { + super.logInfo(log, message); + } + + @Override + protected void logWarn(Logger log, String message) { + super.logWarn(log, message); + } + + /** + * Gets the AccessMode. + * + * @return the {@link AccessMode} + */ + protected abstract AccessMode getAccessMode(); + + /** + * Gets the Component. Be aware, that it might be 'disabled'. + * + * @param componentId the Component-ID + * + * @return the {@link ModbusSlave} Component; possibly null + */ + protected ModbusSlave getPossiblyDisabledComponent(String componentId) { + if (componentId == null) { + return null; + } + if (componentId == Meta.SINGLETON_COMPONENT_ID) { + return this.config.metaComponent(); + } + return this._components.stream() // + .filter(c -> componentId.equals(c.id())) // + .findFirst() // + .orElse(null); + } + + /** + * Adds a Record to the process image at the given address. + * + * @param address the address + * @param record the record + * @param component the OpenEMS Component + * @return the next address after this record + */ + private int addRecordToProcessImage(int address, ModbusRecord record, OpenemsComponent component) { + record.setComponentId(component.id()); + + // Handle writes to the Channel; limited to ModbusRecordChannels + if (record instanceof ModbusRecordChannel) { + var r = (ModbusRecordChannel) record; + r.onWriteValue(value -> { + Channel readChannel = component.channel(r.getChannelId()); + if (!(readChannel instanceof WriteChannel)) { + this.logWarn(this.log, "Unable to write to Read-Only-Channel [" + readChannel.address() + "]"); + return; + } + WriteChannel channel = (WriteChannel) readChannel; + this.apiWorker.addValue(channel, new WritePojo(value)); + }); + } + + this.records.put(address, record); + return address + record.getType().getWords(); + } + + /** + * Initialize Modbus-Records for all configured Component-IDs. + * + * @param metaComponent the {@link Meta} component + * @param componentIds the configured Component-IDs. + */ + private void initializeModbusRecords(Meta metaComponent, String[] componentIds) { + this.records.clear(); + // Add generic header + this.records.put(0, new ModbusRecordUint16Hash(0, "OpenEMS")); + var nextAddress = 1; + + // add Meta-Component + nextAddress = this.addMetaComponentToProcessImage(nextAddress, metaComponent); + + // add remaining components; sorted by configured componentIds + for (String id : componentIds) { + // find next component in order + var component = this.getPossiblyDisabledComponent(id); + if (component == null) { // This should never happen + this.logWarn(this.log, "Required Component [" + id + "] " // + + "is not available. Component may not implement ModbusSlave or is not active."); + continue; + } + + nextAddress = this.addComponentToProcessImage(nextAddress, component); + } + } + + /** + * Adds the Meta-Component to the Process Image. + * + * @param startAddress the start-address + * @param component the {@link Meta} component + * @return the next start-address + */ + private int addMetaComponentToProcessImage(int startAddress, Meta component) { + var table = component.getModbusSlaveTable(this.getAccessMode()); + + // add the Component-Model Length + var nextAddress = this.addRecordToProcessImage(startAddress, + new ModbusRecordUint16BlockLength(-1, component.id(), (short) table.getLength()), component); + + // add Records + for (ModbusSlaveNatureTable natureTable : table.getNatureTables()) { + for (ModbusRecord record : natureTable.getModbusRecords()) { + this.addRecordToProcessImage(nextAddress + record.getOffset(), record, component); + } + } + return startAddress + table.getLength(); + } + + /** + * Adds a Component to the Process Image. + * + * @param startAddress the start-address + * @param component the OpenEMS Component + * @return the next start-address + */ + private int addComponentToProcessImage(int startAddress, ModbusSlave component) { + this.components.put(startAddress, component.alias()); + var table = component.getModbusSlaveTable(this.getAccessMode()); + + // add the Component-ID and Component-Model Length + var nextAddress = this.addRecordToProcessImage(startAddress, + new ModbusRecordString16(-1, "Component-ID", component.id()), component); + this.addRecordToProcessImage(nextAddress, + new ModbusRecordUint16BlockLength(-1, component.id(), (short) table.getLength()), component); + nextAddress = startAddress + 20; + var nextNatureAddress = nextAddress; + + // add all Nature-Tables + for (ModbusSlaveNatureTable natureTable : table.getNatureTables()) { + // add the Interface Hash-Code and Length + nextAddress = this.addRecordToProcessImage(nextNatureAddress, + new ModbusRecordUint16Hash(-1, natureTable.getNatureName()), component); + nextAddress = this.addRecordToProcessImage(nextAddress, + new ModbusRecordUint16BlockLength(-1, natureTable.getNatureName(), (short) natureTable.getLength()), + component); + + // add Records + for (ModbusRecord record : natureTable.getModbusRecords()) { + this.addRecordToProcessImage(nextNatureAddress + 2 + record.getOffset(), record, component); + } + + nextNatureAddress = nextNatureAddress += natureTable.getLength(); + } + + // calculate next address after this component + return startAddress + table.getLength(); + } + + @Override + public void buildJsonApiRoutes(JsonApiBuilder builder) { + builder.handleRequest(GetModbusProtocolRequest.METHOD, def -> { + def.setGuards(this.componentMissingGuard(), this.componentNoModbusApiGuard()); + }, call -> { + return new GetModbusProtocolResponse(call.getRequest().getId(), this.records); + }); + builder.handleRequest(GetModbusProtocolExportXlsxRequest.METHOD, def -> { + def.setGuards(this.componentMissingGuard(), this.componentNoModbusApiGuard()); + }, call -> { + return new GetModbusProtocolExportXlsxResponse(call.getRequest().getId(), this.components, this.records); + }); + } + + private JsonrpcEndpointGuard componentMissingGuard() { + return call -> { + if (this.getComponentMissingFault().get() == true) { + throw new OpenemsException(this.getComponentMissingFaultChannel().channelDoc().getText()); + } + }; + } + + private JsonrpcEndpointGuard componentNoModbusApiGuard() { + return call -> { + if (this.getComponentNoModbusApiFault().get() == true) { + throw new OpenemsException(this.getComponentNoModbusApiFaultChannel().channelDoc().getText()); + } + }; + } + + /** + * Format a given channelAddress to a ChannelId. + * + * @param channel WriteChannel + * @return component_channelId as String + */ + public static String formatChannelName(WriteChannel channel) { + return channel.getComponent().id() + "_" + channel.channelId().name(); + } + +} diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusConfig.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusConfig.java new file mode 100644 index 00000000000..a9792bd4199 --- /dev/null +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusConfig.java @@ -0,0 +1,108 @@ +package io.openems.edge.controller.api.modbus; + +import java.util.Arrays; + +import io.openems.edge.common.meta.Meta; + +public abstract class AbstractModbusConfig { + private final String id; + private final String alias; + private final boolean enabled; + private final Meta metaComponent; + private final String[] componentIds; + private final int apiTimeout; + private final int maxConcurrentConnections; + + public AbstractModbusConfig(String id, String alias, boolean enabled, Meta metaComponent, String[] componentIds, + int apiTimeout, int maxConcurrentConnections) { + this.id = id; + this.alias = alias; + this.enabled = enabled; + this.metaComponent = metaComponent; + this.componentIds = componentIds; + this.apiTimeout = apiTimeout; + this.maxConcurrentConnections = maxConcurrentConnections; + } + + /** + * Returns a unique ID for this OpenEMS component. + * + * @return the unique ID + */ + public String id() { + return this.id; + } + + /** + * Returns a unique ID for this OpenEMS component. + * + * @return the unique ID + */ + public String alias() { + return this.alias; + } + + /** + * Is this controller enabled?. + * + * @return boolean + */ + public boolean enabled() { + return this.enabled; + } + + /** + * Returns a metaComponent. + * + * @return the metaComponent + */ + public Meta metaComponent() { + return this.metaComponent; + } + + /** + * Returns an array of component ids. + * + * @return the componentIds + */ + public String[] componentIds() { + return this.componentIds; + } + + /** + * Returns the api timeout. + * + * @return the apiTimeout + */ + public int apiTimeout() { + return this.apiTimeout; + } + + /** + * Returns the max number of concurrent connections. + * + * @return the maxConcurrentConnections + */ + public int maxConcurrentConnections() { + return this.maxConcurrentConnections; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + AbstractModbusConfig config = (AbstractModbusConfig) other; + return this.enabled == config.enabled // + && this.apiTimeout == config.apiTimeout // + && this.maxConcurrentConnections == config.maxConcurrentConnections + && this.id.equals(config.id) // + && this.alias.equals(config.alias) + && this.metaComponent.equals(config.metaComponent) // + && Arrays.equals(this.componentIds, config.componentIds); + } + +} diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusRtuApi.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusRtuApi.java new file mode 100644 index 00000000000..b93209a5702 --- /dev/null +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusRtuApi.java @@ -0,0 +1,100 @@ +package io.openems.edge.controller.api.modbus; + +import io.openems.edge.bridge.modbus.api.Parity; +import io.openems.edge.bridge.modbus.api.Stopbit; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.jsonapi.ComponentJsonApi; +import io.openems.edge.common.meta.Meta; +import io.openems.edge.controller.api.Controller; + +public abstract class AbstractModbusRtuApi extends AbstractModbusApi + implements ModbusApi, Controller, OpenemsComponent, ComponentJsonApi { + + public AbstractModbusRtuApi(String implementationName, + io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds, + io.openems.edge.common.channel.ChannelId[]... furtherInitialChannelIds) { + super(firstInitialChannelIds, furtherInitialChannelIds); + } + + public static class RtuConfig extends AbstractModbusConfig { + private final String portName; + private final int baudRate; + private final int databits; + private final Stopbit stopbits; + private final Parity parity; + + public RtuConfig(String id, String alias, boolean enabled, Meta metaComponent, String[] componentIds, + int apiTimeout, String portName, int baudRate, int databits, Stopbit stopbits, Parity parity, + int maxConcurrentConnections) { + super(id, alias, enabled, metaComponent, componentIds, apiTimeout, maxConcurrentConnections); + this.portName = portName; + this.baudRate = baudRate; + this.databits = databits; + this.stopbits = stopbits; + this.parity = parity; + } + + /** + * Returns the portName. + * + * @return the portName + */ + public String portName() { + return this.portName; + } + + /** + * Returns the baudRate. + * + * @return the baudRate + */ + public int baudRate() { + return this.baudRate; + } + + /** + * Returns the databits. + * + * @return databits + */ + public int databits() { + return this.databits; + } + + /** + * Returns the stopbits. + * + * @return the stopbits + */ + public Stopbit stopbits() { + return this.stopbits; + } + + /** + * Returns the parity. + * + * @return the parity + */ + public Parity parity() { + return this.parity; + } + + @Override + public boolean equals(Object other) { + if (!super.equals(other)) { + return false; + } + if (!(other instanceof RtuConfig)) { + return false; + } + RtuConfig config = (RtuConfig) other; + return this.baudRate == config.baudRate // + && this.databits == config.databits // + && this.stopbits == config.stopbits // + && this.parity == config.parity // + && this.portName.equals(config.portName); + } + + } + +} diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusTcpApi.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusTcpApi.java index 7aff8bc95ec..066ae09248f 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusTcpApi.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusTcpApi.java @@ -1,531 +1,44 @@ package io.openems.edge.controller.api.modbus; -import java.util.Arrays; -import java.util.List; -import java.util.Map.Entry; -import java.util.TreeMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; - -import org.osgi.service.cm.ConfigurationAdmin; -import org.osgi.service.component.ComponentContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.ghgande.j2mod.modbus.ModbusException; -import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; - -import io.openems.common.channel.AccessMode; -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.exceptions.OpenemsException; -import io.openems.common.utils.ConfigUtils; -import io.openems.common.worker.AbstractWorker; -import io.openems.edge.common.channel.Channel; -import io.openems.edge.common.channel.WriteChannel; -import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; -import io.openems.edge.common.jsonapi.ComponentJsonApi; -import io.openems.edge.common.jsonapi.JsonApiBuilder; -import io.openems.edge.common.jsonapi.JsonrpcEndpointGuard; import io.openems.edge.common.meta.Meta; -import io.openems.edge.common.modbusslave.ModbusRecord; -import io.openems.edge.common.modbusslave.ModbusRecordChannel; -import io.openems.edge.common.modbusslave.ModbusRecordCycleValue; -import io.openems.edge.common.modbusslave.ModbusRecordString16; -import io.openems.edge.common.modbusslave.ModbusRecordUint16BlockLength; -import io.openems.edge.common.modbusslave.ModbusRecordUint16Hash; -import io.openems.edge.common.modbusslave.ModbusSlave; -import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable; import io.openems.edge.controller.api.Controller; -import io.openems.edge.controller.api.common.ApiWorker; -import io.openems.edge.controller.api.common.ApiWorker.WriteHandler; -import io.openems.edge.controller.api.common.Status; -import io.openems.edge.controller.api.common.WriteObject; -import io.openems.edge.controller.api.common.WritePojo; -import io.openems.edge.controller.api.modbus.jsonrpc.GetModbusProtocolExportXlsxRequest; -import io.openems.edge.controller.api.modbus.jsonrpc.GetModbusProtocolExportXlsxResponse; -import io.openems.edge.controller.api.modbus.jsonrpc.GetModbusProtocolRequest; -import io.openems.edge.controller.api.modbus.jsonrpc.GetModbusProtocolResponse; -public abstract class AbstractModbusTcpApi extends AbstractOpenemsComponent - implements ModbusTcpApi, Controller, OpenemsComponent, ComponentJsonApi { +public abstract class AbstractModbusTcpApi extends AbstractModbusApi + implements ModbusApi, Controller, OpenemsComponent { - public static final int UNIT_ID = 1; public static final int DEFAULT_PORT = 502; - public static final int DEFAULT_MAX_CONCURRENT_CONNECTIONS = 5; - - /** - * Holds the link between Modbus address and ModbusRecord. - */ - protected final TreeMap records = new TreeMap<>(); - protected final ApiWorker apiWorker = new ApiWorker(this, - new WriteHandler(this.handleWrites(), this::setOverrideStatus, this.handleTimeouts())); - private final Logger log = LoggerFactory.getLogger(AbstractModbusTcpApi.class); - private final MyProcessImage processImage; - private final String implementationName; - - /** - * Holds the link between Modbus start address of a Component and the - * Component-ID. - */ - private final TreeMap components = new TreeMap<>(); - - private ConfigRecord config; - private List invalidComponents = new CopyOnWriteArrayList<>(); - - protected synchronized void addComponent(OpenemsComponent component) { - if (!(component instanceof ModbusSlave)) { - this.logError(this.log, "Component [" + component.id() + "] does not implement ModbusSlave"); - this.invalidComponents.add(component); - this._setComponentNoModbusApiFault(true); - return; - } - this._components.add((ModbusSlave) component); - this.updateComponents(); - } - - protected abstract Consumer, WriteObject>> handleWrites(); - - protected abstract void setOverrideStatus(Status status); - - protected abstract Runnable handleTimeouts(); - - protected synchronized void removeComponent(OpenemsComponent component) { - this._components.remove(component); - if (this.invalidComponents.remove(component)) { - if (this.invalidComponents.isEmpty()) { - this._setComponentNoModbusApiFault(false); - } - return; - } - this.updateComponents(); - } - - private volatile List _components = new CopyOnWriteArrayList<>(); public AbstractModbusTcpApi(String implementationName, io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds, io.openems.edge.common.channel.ChannelId[]... furtherInitialChannelIds) { super(firstInitialChannelIds, furtherInitialChannelIds); - this.implementationName = implementationName; - this.processImage = new MyProcessImage(this); - } - - protected void activate(ComponentContext context, ConfigurationAdmin cm, ConfigRecord config) - throws OpenemsException { - super.activate(context, config.id(), config.alias(), config.enabled()); - this.handleActivate(config, cm, config.id()); - } - - protected void modified(ComponentContext context, ConfigurationAdmin cm, ConfigRecord config) - throws OpenemsException { - super.modified(context, config.id(), config.alias(), config.enabled()); - - // update filter for 'Components'; allow disable components - final var filter = ConfigUtils.generateReferenceTargetFilter(this.servicePid(), false, config.componentIds); - OpenemsComponent.updateReferenceFilterRaw(cm, this.servicePid(), "Component", filter); - - // Config (relevant for API) was not modified - if (this.config.equals(config)) { - return; - } - - ModbusSlaveFactory.close(); - - // Activate with new config - this.handleModified(config, cm, config.id()); - } - - private void handleActivate(ConfigRecord config, ConfigurationAdmin cm, String id) { - // configuration settings - this.config = config; - - // update filter for 'Components'; allow disable components - final var filter = ConfigUtils.generateReferenceTargetFilter(this.servicePid(), false, config.componentIds); - OpenemsComponent.updateReferenceFilterRaw(cm, this.servicePid(), "Component", filter); - - this.apiWorker.setTimeoutSeconds(config.apiTimeout); - - if (!this.isEnabled()) { - // abort if disabled - return; - } - - // Start Modbus-Server - this.startApiWorker.activate(id); - - this.updateComponents(); - } - - private void handleModified(ConfigRecord config, ConfigurationAdmin cm, String id) { - // configuration settings - this.config = config; - - this.apiWorker.setTimeoutSeconds(config.apiTimeout); - - if (!this.isEnabled()) { - // abort if disabled - this.startApiWorker.deactivate(); - return; - } - - // Modify Modbus-Server - this.startApiWorker.modified(id); - - this.updateComponents(); - } - - /** - * Called by addComponent/removeComponent. Initializes the ModbusRecords, once - * all Components are available. Fault-State otherwise. - */ - private synchronized void updateComponents() { - // Check if all Components are available - var config = this.config; - if (config == null) { - return; - } - if (config.componentIds.length > this._components.size()) { - if (this.getComponentNoModbusApiFaultChannel().getNextValue().get() != Boolean.TRUE) { - this._setComponentMissingFault(true); // Either this or that fault - } - return; - } - this._setComponentMissingFault(false); - - // Initialize Modbus Records - this.initializeModbusRecords(this.config.metaComponent, this.config.componentIds); - } - - @Override - protected void deactivate() { - - this.startApiWorker.deactivate(); - ModbusSlaveFactory.close(); - super.deactivate(); - - // wait until modbus slave was completely closed - try { - Thread.sleep(10000); - } catch (InterruptedException e) { - this.log.warn(e.getMessage()); - } - } - - private final AbstractWorker startApiWorker = new AbstractWorker() { - - private static final int DEFAULT_WAIT_TIME = 5000; // 5 seconds - - private final Logger log = LoggerFactory.getLogger(AbstractWorker.class); - - private com.ghgande.j2mod.modbus.slave.ModbusSlave slave = null; - - @Override - protected void forever() { - var port = AbstractModbusTcpApi.this.config.port; - if (this.slave == null) { - try { - // start new server - this.slave = ModbusSlaveFactory.createTCPSlave(port, - AbstractModbusTcpApi.this.config.maxConcurrentConnections); - this.slave.addProcessImage(UNIT_ID, AbstractModbusTcpApi.this.processImage); - if (isEnabled()) { - this.slave.open(); - AbstractModbusTcpApi.this.logInfo(this.log, - AbstractModbusTcpApi.this.implementationName + " started " // - + "on port [" + port + "] with UnitId [" + AbstractModbusTcpApi.UNIT_ID + "]."); - } - } catch (ModbusException e) { - ModbusSlaveFactory.close(); - AbstractModbusTcpApi.this.logError(this.log, - "Unable to start " + AbstractModbusTcpApi.this.implementationName + " on port [" + port - + "]: " + e.getMessage()); - AbstractModbusTcpApi.this._setUnableToStart(true); - } - - } else { - // regular check for errors - var error = this.slave.getError(); - if (error == null) { - AbstractModbusTcpApi.this._setUnableToStart(false); - - } else { - AbstractModbusTcpApi.this.logError(this.log, - "Unable to start Modbus/TCP Api on port [" + port + "]: " + error); - AbstractModbusTcpApi.this._setUnableToStart(true); - this.slave = null; - // stop server - ModbusSlaveFactory.close(); - } - } - } - - @Override - protected int getCycleTime() { - return DEFAULT_WAIT_TIME; - } - - }; - - /** - * Initialize Modbus-Records for all configured Component-IDs. - * - * @param metaComponent the {@link Meta} component - * @param componentIds the configured Component-IDs. - */ - private void initializeModbusRecords(Meta metaComponent, String[] componentIds) { - // Add generic header - this.records.clear(); - this.records.put(0, new ModbusRecordUint16Hash(0, "OpenEMS")); - var nextAddress = 1; - - // add Meta-Component - nextAddress = this.addMetaComponentToProcessImage(nextAddress, metaComponent); - - // add remaining components; sorted by configured componentIds - for (String id : componentIds) { - // find next component in order - var component = this.getPossiblyDisabledComponent(id); - if (component == null) { // This should never happen - this.logWarn(this.log, "Required Component [" + id + "] " // - + "is not available. Component may not implement ModbusSlave or is not active."); - continue; - } - - // add component to process image - nextAddress = this.addComponentToProcessImage(nextAddress, component); - } - } - - /** - * Adds the Meta-Component to the Process Image. - * - * @param startAddress the start-address - * @param component the {@link Meta} component - * @return the next start-address - */ - private int addMetaComponentToProcessImage(int startAddress, Meta component) { - var table = component.getModbusSlaveTable(this.getAccessMode()); - - // add the Component-Model Length - var nextAddress = this.addRecordToProcessImage(startAddress, - new ModbusRecordUint16BlockLength(-1, component.id(), (short) table.getLength()), component); - - // add Records - for (ModbusSlaveNatureTable natureTable : table.getNatureTables()) { - for (ModbusRecord record : natureTable.getModbusRecords()) { - this.addRecordToProcessImage(nextAddress + record.getOffset(), record, component); - } - } - return startAddress + table.getLength(); } - /** - * Adds a Component to the Process Image. - * - * @param startAddress the start-address - * @param component the OpenEMS Component - * @return the next start-address - */ - private int addComponentToProcessImage(int startAddress, ModbusSlave component) { - this.components.put(startAddress, component.alias()); - var table = component.getModbusSlaveTable(this.getAccessMode()); - - // add the Component-ID and Component-Model Length - var nextAddress = this.addRecordToProcessImage(startAddress, - new ModbusRecordString16(-1, "Component-ID", component.id()), component); - this.addRecordToProcessImage(nextAddress, - new ModbusRecordUint16BlockLength(-1, component.id(), (short) table.getLength()), component); - nextAddress = startAddress + 20; - var nextNatureAddress = nextAddress; + public class TcpConfig extends AbstractModbusConfig { + private final int port; - // add all Nature-Tables - for (ModbusSlaveNatureTable natureTable : table.getNatureTables()) { - // add the Interface Hash-Code and Length - nextAddress = this.addRecordToProcessImage(nextNatureAddress, - new ModbusRecordUint16Hash(-1, natureTable.getNatureName()), component); - nextAddress = this.addRecordToProcessImage(nextAddress, - new ModbusRecordUint16BlockLength(-1, natureTable.getNatureName(), (short) natureTable.getLength()), - component); - - // add Records - for (ModbusRecord record : natureTable.getModbusRecords()) { - this.addRecordToProcessImage(nextNatureAddress + 2 + record.getOffset(), record, component); - } - - nextNatureAddress = nextNatureAddress += natureTable.getLength(); + public TcpConfig(String id, String alias, boolean enabled, Meta metaComponent, String[] componentIds, + int apiTimeout, int port, int maxConcurrentConnections) { + super(id, alias, enabled, metaComponent, componentIds, apiTimeout, maxConcurrentConnections); + this.port = port; } - // calculate next address after this component - return startAddress + table.getLength(); - } - - /** - * Adds a Record to the process image at the given address. - * - * @param address the address - * @param record the record - * @param component the OpenEMS Component - * @return the next address after this record - */ - private int addRecordToProcessImage(int address, ModbusRecord record, OpenemsComponent component) { - record.setComponentId(component.id()); - - // Handle writes to the Channel; limited to ModbusRecordChannels - if (record instanceof ModbusRecordChannel) { - var r = (ModbusRecordChannel) record; - r.onWriteValue(value -> { - Channel readChannel = component.channel(r.getChannelId()); - if (!(readChannel instanceof WriteChannel)) { - this.logWarn(this.log, "Unable to write to Read-Only-Channel [" + readChannel.address() + "]"); - return; - } - WriteChannel channel = (WriteChannel) readChannel; - this.apiWorker.addValue(channel, new WritePojo(value)); - }); + public int getPort() { + return this.port; } - this.records.put(address, record); - return address + record.getType().getWords(); - } - - @Override - public void run() throws OpenemsNamedException { - // Enabled? - if (!this.isEnabled()) { - return; - } - - this.updateCycleValues(); - this.apiWorker.run(); - } - - @SuppressWarnings("unchecked") - /** - * Once every cycle: update the values for each registered - * {@link ModbusRecordCycleValue}. - */ - private void updateCycleValues() { - this.records.values() // - .stream() // - .filter(r -> r instanceof ModbusRecordCycleValue) // - .map(r -> (ModbusRecordCycleValue) r) // - .forEach(r -> { - OpenemsComponent component = this.getPossiblyDisabledComponent(r.getComponentId()); - if (component != null && component.isEnabled()) { - r.updateValue(component); - } else { - r.updateValue(null); - } - }); - } - - @Override - protected void logDebug(Logger log, String message) { - super.logDebug(log, message); - } - - @Override - protected void logInfo(Logger log, String message) { - super.logInfo(log, message); - } - - @Override - protected void logWarn(Logger log, String message) { - super.logWarn(log, message); - } - - @Override - public void buildJsonApiRoutes(JsonApiBuilder builder) { - builder.handleRequest(GetModbusProtocolRequest.METHOD, def -> { - def.setGuards(this.componentMissingGuard(), this.componentNoModbusApiGuard()); - }, call -> { - return new GetModbusProtocolResponse(call.getRequest().getId(), this.records); - }); - builder.handleRequest(GetModbusProtocolExportXlsxRequest.METHOD, def -> { - def.setGuards(this.componentMissingGuard(), this.componentNoModbusApiGuard()); - }, call -> { - return new GetModbusProtocolExportXlsxResponse(call.getRequest().getId(), this.components, this.records); - }); - } - - private JsonrpcEndpointGuard componentMissingGuard() { - return call -> { - if (this.getComponentMissingFault().get() == Boolean.TRUE) { - throw new OpenemsException(this.getComponentMissingFaultChannel().channelDoc().getText()); - } - }; - } - - private JsonrpcEndpointGuard componentNoModbusApiGuard() { - return call -> { - if (this.getComponentNoModbusApiFault().get() == Boolean.TRUE) { - throw new OpenemsException(this.getComponentNoModbusApiFaultChannel().channelDoc().getText()); - } - }; - } - - /** - * Gets the AccessMode. - * - * @return the {@link AccessMode} - */ - protected abstract AccessMode getAccessMode(); - - /** - * Gets the Component. Be aware, that it might be 'disabled'. - * - * @param componentId the Component-ID - * - * @return the {@link ModbusSlave} Component; possibly null - */ - protected ModbusSlave getPossiblyDisabledComponent(String componentId) { - if (componentId == null) { - return null; - } - if (componentId == Meta.SINGLETON_COMPONENT_ID) { - return this.config.metaComponent; - } - return this._components.stream() // - .filter(c -> componentId.equals(c.id())) // - .findFirst() // - .orElse(null); - } - - public static record ConfigRecord(String id, String alias, boolean enabled, Meta metaComponent, - String[] componentIds, int apiTimeout, int port, int maxConcurrentConnections) { - @Override public boolean equals(Object other) { - if (this == other) { - return true; - } - if (other == null) { + if (!super.equals(other)) { return false; } - if (!(other instanceof ConfigRecord)) { + if (!(other instanceof TcpConfig)) { return false; } - ConfigRecord config = (ConfigRecord) other; - - if (config.id.equals(this.id) && config.alias.equals(this.alias) // - && config.enabled == this.enabled && config.metaComponent.equals(this.metaComponent) // - && Arrays.equals(config.componentIds, this.componentIds) // - && config.apiTimeout == this.apiTimeout && config.port == this.port // - && config.maxConcurrentConnections == this.maxConcurrentConnections) { - return true; - } - return false; + TcpConfig config = (TcpConfig) other; + return this.port == config.port; } - } - /** - * Format a given channelAddress to a ChannelId. - * - * @param channel WriteChannel - * @return component_channelId as String - */ - public static String formatChannelName(WriteChannel channel) { - return channel.getComponent().id() + "_" + channel.channelId().name(); } } diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/ModbusTcpApi.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/ModbusApi.java similarity index 97% rename from io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/ModbusTcpApi.java rename to io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/ModbusApi.java index da838a2d947..c7357b08c1d 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/ModbusTcpApi.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/ModbusApi.java @@ -7,11 +7,11 @@ import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.OpenemsComponent; -public interface ModbusTcpApi extends OpenemsComponent { +public interface ModbusApi extends OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { UNABLE_TO_START(Doc.of(Level.FAULT) // - .text("Unable to start Modbus/TCP-Api Server")), // + .text("Unable to start ModbusTCP/RTU-Api Server")), // COMPONENT_NO_MODBUS_API_FAULT(Doc.of(Level.FAULT) // .text("A configured Component does not support Modbus-API")), // COMPONENT_MISSING_FAULT(Doc.of(Level.FAULT) // diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/MyProcessImage.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/MyProcessImage.java index cbc587a8c4f..e155ba3576f 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/MyProcessImage.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/MyProcessImage.java @@ -19,15 +19,15 @@ import io.openems.edge.common.modbusslave.ModbusRecordUint16Reserved; /** - * This implementation answers Modbus-TCP Slave requests. + * This implementation answers Modbus-TCP/RTU Slave requests. */ public class MyProcessImage implements ProcessImage { private final Logger log = LoggerFactory.getLogger(MyProcessImage.class); - protected final AbstractModbusTcpApi parent; + protected final AbstractModbusApi parent; - protected MyProcessImage(AbstractModbusTcpApi parent) { + protected MyProcessImage(AbstractModbusApi parent) { this.parent = parent; } diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/rtu/Config.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/rtu/Config.java new file mode 100644 index 00000000000..d2ca0403017 --- /dev/null +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/rtu/Config.java @@ -0,0 +1,52 @@ +package io.openems.edge.controller.api.modbus.readonly.rtu; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +import io.openems.edge.bridge.modbus.api.Parity; +import io.openems.edge.bridge.modbus.api.Stopbit; +import io.openems.edge.controller.api.modbus.AbstractModbusRtuApi; + +@ObjectClassDefinition(// + name = "Controller Api Modbus/RTU Read-Only", // + description = "This controller provides a read-only Modbus/RTU api.") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "ctrlApiModbusRtu0"; + + @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") + String alias() default "ModbusRtu Read-Only"; + + @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") + boolean enabled() default true; + + @AttributeDefinition(name = "Port-Name", description = "The name of the serial port - e.g. '/dev/ttyUSB0' or 'COM3'") + String portName() default "/dev/ttyUSB0"; + + @AttributeDefinition(name = "Component-IDs", description = "Components that should be made available via Modbus.") + String[] component_ids() default { "_sum" }; + + @AttributeDefinition(name = "Api-Timeout", description = "Sets the timeout in seconds for updates on Channels set by this Api.") + int apiTimeout() default 60; + + @AttributeDefinition(name = "Max concurrent connections", description = "Sets the maximum number of concurrent connections via Modbus.") + int maxConcurrentConnections() default AbstractModbusRtuApi.DEFAULT_MAX_CONCURRENT_CONNECTIONS; + + @AttributeDefinition(name = "Components target filter", description = "This is auto-generated by 'Component-IDs'.") + String Component_target() default "(enabled=true)"; + + @AttributeDefinition(name = "Baudrate", description = "The baudrate - e.g. 9600, 19200, 38400, 57600 or 115200") + int baudRate() default 9600; + + @AttributeDefinition(name = "Databits", description = "The number of databits - e.g. 8") + int databits() default 8; + + @AttributeDefinition(name = "Stopbits", description = "The number of stopbits - '1', '1.5' or '2'") + Stopbit stopbits() default Stopbit.ONE; + + @AttributeDefinition(name = "Parity", description = "The parity - 'none', 'even', 'odd', 'mark' or 'space'") + Parity parity() default Parity.NONE; + + String webconsole_configurationFactory_nameHint() default "Controller Api Modbus/RTU Read-Write [{id}]"; +} \ No newline at end of file diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/rtu/ControllerApiModbusRtuReadOnly.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/rtu/ControllerApiModbusRtuReadOnly.java new file mode 100644 index 00000000000..3695fe79585 --- /dev/null +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/rtu/ControllerApiModbusRtuReadOnly.java @@ -0,0 +1,23 @@ +package io.openems.edge.controller.api.modbus.readonly.rtu; + +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; + +public interface ControllerApiModbusRtuReadOnly extends OpenemsComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + ; + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } +} + diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/rtu/ControllerApiModbusRtuReadOnlyImpl.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/rtu/ControllerApiModbusRtuReadOnlyImpl.java new file mode 100644 index 00000000000..2a6d065a9b1 --- /dev/null +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/rtu/ControllerApiModbusRtuReadOnlyImpl.java @@ -0,0 +1,107 @@ +package io.openems.edge.controller.api.modbus.readonly.rtu; + +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 com.ghgande.j2mod.modbus.Modbus; +import com.ghgande.j2mod.modbus.ModbusException; +import com.ghgande.j2mod.modbus.slave.ModbusSlave; +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; +import com.ghgande.j2mod.modbus.util.SerialParameters; + +import io.openems.common.channel.AccessMode; +import io.openems.common.exceptions.OpenemsException; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.jsonapi.ComponentJsonApi; +import io.openems.edge.common.meta.Meta; +import io.openems.edge.controller.api.Controller; +import io.openems.edge.controller.api.modbus.AbstractModbusRtuApi; +import io.openems.edge.controller.api.modbus.ModbusApi; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Controller.Api.ModbusRtu.ReadOnly", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class ControllerApiModbusRtuReadOnlyImpl extends AbstractModbusRtuApi + implements ControllerApiModbusRtuReadOnly, ModbusApi, Controller, OpenemsComponent, ComponentJsonApi { + + @Reference + private Meta metaComponent = null; + + @Reference + private ConfigurationAdmin cm; + + private RtuConfig config; + + public ControllerApiModbusRtuReadOnlyImpl() { + super("Modbus/RTU-Api Read-Only", // + OpenemsComponent.ChannelId.values(), // + Controller.ChannelId.values(), // + ModbusApi.ChannelId.values(), // + ControllerApiModbusRtuReadOnly.ChannelId.values() // + ); + } + + @Override + @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MULTIPLE) + protected void addComponent(OpenemsComponent component) { + super.addComponent(component); + } + + protected void removeComponent(OpenemsComponent component) { + super.removeComponent(component); + } + + @Activate + private void activate(ComponentContext context, Config config) throws OpenemsException { + this.config = new RtuConfig(config.id(), config.alias(), config.enabled(), this.metaComponent, + config.component_ids(), 0 /* no timeout */, config.portName(), config.baudRate(), config.databits(), + config.stopbits(), config.parity(), config.maxConcurrentConnections()); + super.activate(context, this.cm, this.config); + } + + @Modified + private void modified(ComponentContext context, Config config) throws OpenemsException { + this.config = new RtuConfig(config.id(), config.alias(), config.enabled(), this.metaComponent, + config.component_ids(), 0 /* no timeout */, config.portName(), config.baudRate(), config.databits(), + config.stopbits(), config.parity(), config.maxConcurrentConnections()); + super.modified(context, this.cm, this.config); + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + protected ModbusSlave createSlave() throws ModbusException { + SerialParameters params = new SerialParameters(); + params.setPortName(this.config.portName()); + params.setBaudRate(this.config.baudRate()); + params.setDatabits(this.config.databits()); + params.setStopbits(this.config.stopbits().getValue()); + params.setParity(this.config.parity().getValue()); + params.setEncoding(Modbus.SERIAL_ENCODING_RTU); + params.setEcho(false); + return ModbusSlaveFactory.createSerialSlave(params); + } + + @Override + protected AccessMode getAccessMode() { + return AccessMode.READ_ONLY; + } + +} \ No newline at end of file diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/Config.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/tcp/Config.java similarity index 93% rename from io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/Config.java rename to io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/tcp/Config.java index 56c67020d85..e8947eea421 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/Config.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/tcp/Config.java @@ -1,4 +1,4 @@ -package io.openems.edge.controller.api.modbus.readonly; +package io.openems.edge.controller.api.modbus.readonly.tcp; import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; @@ -14,7 +14,7 @@ String id() default "ctrlApiModbusTcp0"; @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") - String alias() default ""; + String alias() default "ModbusTcp Read-Only"; @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") boolean enabled() default true; diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnly.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/tcp/ControllerApiModbusTcpReadOnly.java similarity index 87% rename from io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnly.java rename to io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/tcp/ControllerApiModbusTcpReadOnly.java index 52bb94c3b2e..cb31ba9d8d4 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnly.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/tcp/ControllerApiModbusTcpReadOnly.java @@ -1,4 +1,4 @@ -package io.openems.edge.controller.api.modbus.readonly; +package io.openems.edge.controller.api.modbus.readonly.tcp; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.component.OpenemsComponent; diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImpl.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/tcp/ControllerApiModbusTcpReadOnlyImpl.java similarity index 74% rename from io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImpl.java rename to io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/tcp/ControllerApiModbusTcpReadOnlyImpl.java index 6f7e36683f8..9924a333487 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImpl.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/tcp/ControllerApiModbusTcpReadOnlyImpl.java @@ -1,4 +1,4 @@ -package io.openems.edge.controller.api.modbus.readonly; +package io.openems.edge.controller.api.modbus.readonly.tcp; import java.util.Map.Entry; import java.util.function.Consumer; @@ -16,9 +16,12 @@ import org.osgi.service.metatype.annotations.Designate; import com.ghgande.j2mod.modbus.ModbusException; +import com.ghgande.j2mod.modbus.slave.ModbusSlave; +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.utils.FunctionUtils; import io.openems.edge.common.channel.WriteChannel; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.jsonapi.ComponentJsonApi; @@ -27,7 +30,7 @@ import io.openems.edge.controller.api.common.Status; import io.openems.edge.controller.api.common.WriteObject; import io.openems.edge.controller.api.modbus.AbstractModbusTcpApi; -import io.openems.edge.controller.api.modbus.ModbusTcpApi; +import io.openems.edge.controller.api.modbus.ModbusApi; @Designate(ocd = Config.class, factory = true) @Component(// @@ -36,14 +39,16 @@ configurationPolicy = ConfigurationPolicy.REQUIRE // ) public class ControllerApiModbusTcpReadOnlyImpl extends AbstractModbusTcpApi - implements ControllerApiModbusTcpReadOnly, ModbusTcpApi, Controller, OpenemsComponent, ComponentJsonApi { + implements ControllerApiModbusTcpReadOnly, ModbusApi, Controller, OpenemsComponent, ComponentJsonApi { - @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + @Reference private Meta metaComponent = null; @Reference private ConfigurationAdmin cm; + private TcpConfig config; + @Override @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MULTIPLE) protected void addComponent(OpenemsComponent component) { @@ -58,16 +63,16 @@ public ControllerApiModbusTcpReadOnlyImpl() { super("Modbus/TCP-Api Read-Only", // OpenemsComponent.ChannelId.values(), // Controller.ChannelId.values(), // - ModbusTcpApi.ChannelId.values(), // + ModbusApi.ChannelId.values(), // ControllerApiModbusTcpReadOnly.ChannelId.values() // ); } @Activate private void activate(ComponentContext context, Config config) throws ModbusException, OpenemsException { - super.activate(context, this.cm, - new ConfigRecord(config.id(), config.alias(), config.enabled(), this.metaComponent, - config.component_ids(), 0 /* no timeout */, config.port(), config.maxConcurrentConnections())); + this.config = new TcpConfig(config.id(), config.alias(), config.enabled(), this.metaComponent, + config.component_ids(), 0 /* no timeout */, config.port(), config.maxConcurrentConnections()); + super.activate(context, this.cm, this.config); } @Override @@ -83,8 +88,7 @@ protected AccessMode getAccessMode() { @Override protected Consumer, WriteObject>> handleWrites() { - return entry -> { - }; + return FunctionUtils::doNothing; } @Override @@ -93,7 +97,11 @@ protected void setOverrideStatus(Status status) { @Override protected Runnable handleTimeouts() { - return () -> { - }; + return FunctionUtils::doNothing; + } + + @Override + protected ModbusSlave createSlave() throws ModbusException { + return ModbusSlaveFactory.createTCPSlave(this.config.getPort(), this.config.maxConcurrentConnections()); } } diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/rtu/Config.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/rtu/Config.java new file mode 100644 index 00000000000..380d24fb662 --- /dev/null +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/rtu/Config.java @@ -0,0 +1,52 @@ +package io.openems.edge.controller.api.modbus.readwrite.rtu; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +import io.openems.edge.bridge.modbus.api.Parity; +import io.openems.edge.bridge.modbus.api.Stopbit; +import io.openems.edge.controller.api.modbus.AbstractModbusRtuApi; + +@ObjectClassDefinition(// + name = "Controller Api Modbus/RTU Read-Write", // + description = "This controller provides a read-write Modbus/RTU api.") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "ctrlApiModbusRtu0"; + + @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") + String alias() default "ModbusRtu Read-Write"; + + @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") + boolean enabled() default true; + + @AttributeDefinition(name = "Port-Name", description = "The name of the serial port - e.g. '/dev/ttyUSB0' or 'COM3'") + String portName() default "/dev/ttyUSB0"; + + @AttributeDefinition(name = "Component-IDs", description = "Components that should be made available via Modbus.") + String[] component_ids() default { "_sum" }; + + @AttributeDefinition(name = "Api-Timeout", description = "Sets the timeout in seconds for updates on Channels set by this Api.") + int apiTimeout() default 60; + + @AttributeDefinition(name = "Max concurrent connections", description = "Sets the maximum number of concurrent connections via Modbus.") + int maxConcurrentConnections() default AbstractModbusRtuApi.DEFAULT_MAX_CONCURRENT_CONNECTIONS; + + @AttributeDefinition(name = "Components target filter", description = "This is auto-generated by 'Component-IDs'.") + String Component_target() default "(enabled=true)"; + + @AttributeDefinition(name = "Baudrate", description = "The baudrate - e.g. 9600, 19200, 38400, 57600 or 115200") + int baudRate() default 9600; + + @AttributeDefinition(name = "Databits", description = "The number of databits - e.g. 8") + int databits() default 8; + + @AttributeDefinition(name = "Stopbits", description = "The number of stopbits - '1', '1.5' or '2'") + Stopbit stopbits() default Stopbit.ONE; + + @AttributeDefinition(name = "Parity", description = "The parity - 'none', 'even', 'odd', 'mark' or 'space'") + Parity parity() default Parity.NONE; + + String webconsole_configurationFactory_nameHint() default "Controller Api Modbus/RTU Read-Write [{id}]"; +} \ No newline at end of file diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/rtu/ControllerApiModbusRtuReadWrite.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/rtu/ControllerApiModbusRtuReadWrite.java new file mode 100644 index 00000000000..85280084c66 --- /dev/null +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/rtu/ControllerApiModbusRtuReadWrite.java @@ -0,0 +1,50 @@ +package io.openems.edge.controller.api.modbus.readwrite.rtu; + +import io.openems.common.channel.Unit; +import io.openems.common.types.OpenemsType; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.channel.StringReadChannel; +import io.openems.edge.common.component.OpenemsComponent; + +public interface ControllerApiModbusRtuReadWrite extends OpenemsComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + API_WORKER_LOG(Doc.of(OpenemsType.STRING) // + .text("Logs Write-Commands via ApiWorker")), // + DEBUG_SET_ACTIVE_POWER_EQUALS(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT)), // + DEBUG_SET_ACTIVE_POWER_GREATER_OR_EQUALS(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT)), // + DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT)), // + DEBUG_SET_REACTIVE_POWER_EQUALS(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.VOLT_AMPERE_REACTIVE)), // + DEBUG_SET_REACTIVE_POWER_GREATER_OR_EQUALS(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.VOLT_AMPERE_REACTIVE)), // + DEBUG_SET_REACTIVE_POWER_LESS_OR_EQUALS(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.VOLT_AMPERE_REACTIVE)), // + + ; + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + /** + * Gets the Channel for {@link ChannelId#API_WORKER_LOG}. + * + * @return the Channel + */ + public default StringReadChannel getApiWorkerLogChannel() { + return this.channel(ChannelId.API_WORKER_LOG); + } + +} \ No newline at end of file diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/rtu/ControllerApiModbusRtuReadWriteImpl.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/rtu/ControllerApiModbusRtuReadWriteImpl.java new file mode 100644 index 00000000000..7cfd34c5840 --- /dev/null +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/rtu/ControllerApiModbusRtuReadWriteImpl.java @@ -0,0 +1,141 @@ +package io.openems.edge.controller.api.modbus.readwrite.rtu; + +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 com.ghgande.j2mod.modbus.Modbus; +import com.ghgande.j2mod.modbus.ModbusException; +import com.ghgande.j2mod.modbus.slave.ModbusSlave; +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; +import com.ghgande.j2mod.modbus.util.SerialParameters; + +import io.openems.common.channel.AccessMode; +import io.openems.common.exceptions.OpenemsException; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.jsonapi.ComponentJsonApi; +import io.openems.edge.common.meta.Meta; +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.controller.api.Controller; +import io.openems.edge.controller.api.modbus.AbstractModbusRtuApi; +import io.openems.edge.controller.api.modbus.ModbusApi; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Controller.Api.ModbusRtu.ReadWrite", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class ControllerApiModbusRtuReadWriteImpl extends AbstractModbusRtuApi + implements ControllerApiModbusRtuReadWrite, ModbusApi, Controller, OpenemsComponent, ComponentJsonApi, + io.openems.edge.common.modbusslave.ModbusSlave { + + @Reference + private Meta metaComponent = null; + + @Reference + private ConfigurationAdmin cm; + + private RtuConfig config; + + @Reference + private ComponentManager componentManager; + + public ControllerApiModbusRtuReadWriteImpl() { + super("Modbus/RTU-Api Read-Write", // + OpenemsComponent.ChannelId.values(), // + Controller.ChannelId.values(), // + ModbusApi.ChannelId.values(), // + ControllerApiModbusRtuReadWrite.ChannelId.values() // + ); + this.apiWorker.setLogChannel(this.getApiWorkerLogChannel()); + } + + @Override + @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MULTIPLE) + protected void addComponent(OpenemsComponent component) { + super.addComponent(component); + } + + @Override + protected void removeComponent(OpenemsComponent component) { + super.removeComponent(component); + } + + @Activate + private void activate(ComponentContext context, Config config) throws OpenemsException { + this.config = new RtuConfig(config.id(), config.alias(), config.enabled(), this.metaComponent, + config.component_ids(), config.apiTimeout(), config.portName(), config.baudRate(), config.databits(), + config.stopbits(), config.parity(), config.maxConcurrentConnections()); + super.activate(context, this.cm, this.config); + } + + @Modified + private void modified(ComponentContext context, Config config) throws OpenemsException { + this.config = new RtuConfig(config.id(), config.alias(), config.enabled(), this.metaComponent, + config.component_ids(), config.apiTimeout(), config.portName(), config.baudRate(), config.databits(), + config.stopbits(), config.parity(), config.maxConcurrentConnections()); + super.modified(context, this.cm, this.config); + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + protected ModbusSlave createSlave() throws ModbusException { + SerialParameters params = new SerialParameters(); + params.setPortName(this.config.portName()); + params.setBaudRate(this.config.baudRate()); + params.setDatabits(this.config.databits()); + params.setStopbits(this.config.stopbits().getValue()); + params.setParity(this.config.parity().getValue()); + params.setEncoding(Modbus.SERIAL_ENCODING_RTU); + params.setEcho(false); + return ModbusSlaveFactory.createSerialSlave(params); + } + + @Override + protected AccessMode getAccessMode() { + return AccessMode.READ_WRITE; + } + + @Override + public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { + return new ModbusSlaveTable(// + OpenemsComponent.getModbusSlaveNatureTable(accessMode), // + ModbusSlaveNatureTable.of(ControllerApiModbusRtuReadWrite.class, accessMode, 100) // + .channel(0, ModbusApi.ChannelId.UNABLE_TO_START, ModbusType.UINT16) // + .channel(1, ModbusApi.ChannelId.COMPONENT_MISSING_FAULT, ModbusType.UINT16) // + .channel(2, ModbusApi.ChannelId.PROCESS_IMAGE_FAULT, ModbusType.UINT16) // + .channel(3, ModbusApi.ChannelId.COMPONENT_NO_MODBUS_API_FAULT, ModbusType.UINT16) // + .channel(4, ControllerApiModbusRtuReadWrite.ChannelId.DEBUG_SET_ACTIVE_POWER_EQUALS, + ModbusType.FLOAT32) // + .channel(6, ControllerApiModbusRtuReadWrite.ChannelId.DEBUG_SET_ACTIVE_POWER_GREATER_OR_EQUALS, + ModbusType.FLOAT32) // + .channel(8, ControllerApiModbusRtuReadWrite.ChannelId.DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, + ModbusType.FLOAT32) // + .channel(10, ControllerApiModbusRtuReadWrite.ChannelId.DEBUG_SET_REACTIVE_POWER_EQUALS, + ModbusType.FLOAT32) // + .channel(12, + ControllerApiModbusRtuReadWrite.ChannelId.DEBUG_SET_REACTIVE_POWER_GREATER_OR_EQUALS, + ModbusType.FLOAT32) // + .channel(14, ControllerApiModbusRtuReadWrite.ChannelId.DEBUG_SET_REACTIVE_POWER_LESS_OR_EQUALS, + ModbusType.FLOAT32) // + .build()); // + } +} \ No newline at end of file diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/Config.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/tcp/Config.java similarity index 94% rename from io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/Config.java rename to io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/tcp/Config.java index 9edd49da1bd..efd08feb754 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/Config.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/tcp/Config.java @@ -1,4 +1,4 @@ -package io.openems.edge.controller.api.modbus.readwrite; +package io.openems.edge.controller.api.modbus.readwrite.tcp; import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; @@ -14,7 +14,7 @@ String id() default "ctrlApiModbusTcp0"; @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") - String alias() default ""; + String alias() default "ModbusTcp Read-Write"; @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") boolean enabled() default true; diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWrite.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/tcp/ControllerApiModbusTcpReadWrite.java similarity index 97% rename from io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWrite.java rename to io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/tcp/ControllerApiModbusTcpReadWrite.java index f39092ee732..47e04f0b476 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWrite.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/tcp/ControllerApiModbusTcpReadWrite.java @@ -1,4 +1,4 @@ -package io.openems.edge.controller.api.modbus.readwrite; +package io.openems.edge.controller.api.modbus.readwrite.tcp; import static io.openems.common.channel.PersistencePriority.HIGH; import static io.openems.common.channel.Unit.CUMULATED_SECONDS; diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImpl.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/tcp/ControllerApiModbusTcpReadWriteImpl.java similarity index 86% rename from io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImpl.java rename to io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/tcp/ControllerApiModbusTcpReadWriteImpl.java index 6f1a47ad49c..6aff04589ce 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImpl.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/tcp/ControllerApiModbusTcpReadWriteImpl.java @@ -1,4 +1,4 @@ -package io.openems.edge.controller.api.modbus.readwrite; +package io.openems.edge.controller.api.modbus.readwrite.tcp; import static io.openems.edge.common.channel.ChannelId.channelIdCamelToUpper; import static io.openems.edge.common.channel.ChannelId.channelIdUpperToCamel; @@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory; import com.ghgande.j2mod.modbus.ModbusException; +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; import io.openems.common.channel.AccessMode; import io.openems.common.channel.PersistencePriority; @@ -48,7 +49,7 @@ import io.openems.edge.controller.api.common.Status; import io.openems.edge.controller.api.common.WriteObject; import io.openems.edge.controller.api.modbus.AbstractModbusTcpApi; -import io.openems.edge.controller.api.modbus.ModbusTcpApi; +import io.openems.edge.controller.api.modbus.ModbusApi; import io.openems.edge.ess.api.ManagedSymmetricEss; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; @@ -61,7 +62,7 @@ configurationPolicy = ConfigurationPolicy.REQUIRE // ) public class ControllerApiModbusTcpReadWriteImpl extends AbstractModbusTcpApi - implements ControllerApiModbusTcpReadWrite, ModbusTcpApi, Controller, OpenemsComponent, ComponentJsonApi, + implements ControllerApiModbusTcpReadWrite, ModbusApi, Controller, OpenemsComponent, ComponentJsonApi, TimedataProvider, ModbusSlave { private final Logger log = LoggerFactory.getLogger(ControllerApiModbusTcpReadWriteImpl.class); @@ -76,12 +77,14 @@ public class ControllerApiModbusTcpReadWriteImpl extends AbstractModbusTcpApi private List components = new ArrayList<>(); + private TcpConfig config; + private boolean isActive = false; @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) private volatile Timedata timedata = null; - @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + @Reference private Meta metaComponent = null; @Reference @@ -107,7 +110,7 @@ public ControllerApiModbusTcpReadWriteImpl() { super("Modbus/TCP-Api Read-Write", // OpenemsComponent.ChannelId.values(), // Controller.ChannelId.values(), // - ModbusTcpApi.ChannelId.values(), // + ModbusApi.ChannelId.values(), // ControllerApiModbusTcpReadWrite.ChannelId.values() // ); this.apiWorker.setLogChannel(this.getApiWorkerLogChannel()); @@ -115,17 +118,17 @@ public ControllerApiModbusTcpReadWriteImpl() { @Activate private void activate(ComponentContext context, Config config) throws ModbusException, OpenemsException { - super.activate(context, this.cm, - new ConfigRecord(config.id(), config.alias(), config.enabled(), this.metaComponent, - config.component_ids(), config.apiTimeout(), config.port(), config.maxConcurrentConnections())); + this.config = new TcpConfig(config.id(), config.alias(), config.enabled(), this.metaComponent, + config.component_ids(), config.apiTimeout(), config.port(), config.maxConcurrentConnections()); + super.activate(context, this.cm, this.config); this.applyConfig(config); } @Modified private void modified(ComponentContext context, Config config) throws OpenemsNamedException { - super.modified(context, this.cm, - new ConfigRecord(config.id(), config.alias(), config.enabled(), this.metaComponent, - config.component_ids(), config.apiTimeout(), config.port(), config.maxConcurrentConnections())); + this.config = new TcpConfig(config.id(), config.alias(), config.enabled(), this.metaComponent, + config.component_ids(), config.apiTimeout(), config.port(), config.maxConcurrentConnections()); + super.modified(context, this.cm, this.config); this.applyConfig(config); } @@ -245,14 +248,18 @@ protected Integer getChannelValue(String componentId, io.openems.edge.common.cha public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { return new ModbusSlaveTable(// OpenemsComponent.getModbusSlaveNatureTable(AccessMode.READ_ONLY), - ModbusSlaveNatureTable.of(ControllerApiModbusTcpReadWriteImpl.class, AccessMode.READ_ONLY, 300) - .cycleValue(0, this.id() + "/ Ess0ActivePowerLimit", Unit.WATT, "", ModbusType.FLOAT32, + ModbusSlaveNatureTable.of(ControllerApiModbusTcpReadWrite.class, AccessMode.READ_ONLY, 300) + .cycleValue(0, this.id() + "/Ess0ActivePowerLimit", Unit.WATT, "", ModbusType.FLOAT32, t -> this.getChannelValue("ess0", ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS)) - .cycleValue(2, this.id() + "/Ess0ReactivePowerLimit", Unit.WATT, "", ModbusType.FLOAT32, - t -> this.getChannelValue("ess0", + .cycleValue(2, this.id() + "/Ess0ReactivePowerLimit", Unit.VOLT_AMPERE_REACTIVE, "", + ModbusType.FLOAT32, t -> this.getChannelValue("ess0", ManagedSymmetricEss.ChannelId.SET_REACTIVE_POWER_EQUALS)) .build()); } + @Override + protected com.ghgande.j2mod.modbus.slave.ModbusSlave createSlave() throws ModbusException { + return ModbusSlaveFactory.createTCPSlave(this.config.getPort(), this.config.maxConcurrentConnections()); + } } diff --git a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/rtu/ControllerApiModbusRtuReadOnlyImplTest.java b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/rtu/ControllerApiModbusRtuReadOnlyImplTest.java new file mode 100644 index 00000000000..251a9bf8145 --- /dev/null +++ b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/rtu/ControllerApiModbusRtuReadOnlyImplTest.java @@ -0,0 +1,32 @@ +package io.openems.edge.controller.api.modbus.readonly.rtu; + +import org.junit.Test; + +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.bridge.modbus.api.Parity; +import io.openems.edge.bridge.modbus.api.Stopbit; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.controller.test.ControllerTest; + +public class ControllerApiModbusRtuReadOnlyImplTest { + + private static final String CTRL_ID = "ctrl0"; + + @Test + public void test() throws Exception { + new ControllerTest(new ControllerApiModbusRtuReadOnlyImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEnabled(false) // do not actually start server + .setParity(Parity.NONE) + .setStopbit(Stopbit.ONE) + .setBaudrate(9600) // + .setComponentIds() // + .setMaxConcurrentConnections(5) // + .setPortName("/dev/ttyUSB0") // + .build()) // + .next(new TestCase()) // + ; + } +} diff --git a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/rtu/MyConfig.java b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/rtu/MyConfig.java new file mode 100644 index 00000000000..4eef1319428 --- /dev/null +++ b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/rtu/MyConfig.java @@ -0,0 +1,147 @@ +package io.openems.edge.controller.api.modbus.readonly.rtu; + +import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.utils.ConfigUtils; +import io.openems.edge.bridge.modbus.api.Parity; +import io.openems.edge.bridge.modbus.api.Stopbit; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + private String id; + private boolean enabled; + private String portName; + private String[] componentIds; + private int baudrate; + private int databits; + private Stopbit stopbit; + private Parity parity; + private int apiTimeout; + private int maxConcurrentConnections; + + private Builder() { + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public Builder setPortName(String portName) { + this.portName = portName; + return this; + } + + public Builder setComponentIds(String... componentIds) { + this.componentIds = componentIds; + return this; + } + + public Builder setMaxConcurrentConnections(int maxConcurrentConnections) { + this.maxConcurrentConnections = maxConcurrentConnections; + return this; + } + + public Builder setBaudrate(int baudrate) { + this.baudrate = baudrate; + return this; + } + + public Builder setDatabits(int databits) { + this.databits = databits; + return this; + } + + public Builder setStopbit(Stopbit stopbit) { + this.stopbit = stopbit; + return this; + } + + public Builder setParity(Parity parity) { + this.parity = parity; + return this; + } + + public Builder setApiTimeout(int apiTimeout) { + this.apiTimeout = apiTimeout; + 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 boolean enabled() { + return this.builder.enabled; + } + + @Override + public String portName() { + return this.builder.portName; + } + + @Override + public int apiTimeout() { + return this.builder.apiTimeout; + } + + @Override + public int baudRate() { + return this.builder.baudrate; + } + + @Override + public int databits() { + return this.builder.baudrate; + } + + @Override + public Stopbit stopbits() { + return this.builder.stopbit; + } + + @Override + public Parity parity() { + return this.builder.parity; + } + + @Override + public String[] component_ids() { + return this.builder.componentIds; + } + + @Override + public int maxConcurrentConnections() { + return this.builder.maxConcurrentConnections; + } + + @Override + public String Component_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), false, this.component_ids()); + } + +} \ No newline at end of file diff --git a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImplTest.java b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/tcp/ControllerApiModbusTcpReadOnlyImplTest.java similarity index 93% rename from io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImplTest.java rename to io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/tcp/ControllerApiModbusTcpReadOnlyImplTest.java index 6248a9b4e41..dc90cfccd44 100644 --- a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImplTest.java +++ b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/tcp/ControllerApiModbusTcpReadOnlyImplTest.java @@ -1,4 +1,4 @@ -package io.openems.edge.controller.api.modbus.readonly; +package io.openems.edge.controller.api.modbus.readonly.tcp; import static io.openems.edge.controller.api.modbus.AbstractModbusTcpApi.DEFAULT_PORT; diff --git a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/MyConfig.java b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/tcp/MyConfig.java similarity index 96% rename from io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/MyConfig.java rename to io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/tcp/MyConfig.java index 0b9618e3806..428f8b1dd1e 100644 --- a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/MyConfig.java +++ b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readonly/tcp/MyConfig.java @@ -1,4 +1,4 @@ -package io.openems.edge.controller.api.modbus.readonly; +package io.openems.edge.controller.api.modbus.readonly.tcp; import io.openems.common.test.AbstractComponentConfig; import io.openems.common.utils.ConfigUtils; diff --git a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/rtu/ControllerApiModbusRtuReadWriteImplTest.java b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/rtu/ControllerApiModbusRtuReadWriteImplTest.java new file mode 100644 index 00000000000..2a3e244447c --- /dev/null +++ b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/rtu/ControllerApiModbusRtuReadWriteImplTest.java @@ -0,0 +1,30 @@ +package io.openems.edge.controller.api.modbus.readwrite.rtu; + +import org.junit.Test; + +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.bridge.modbus.api.Parity; +import io.openems.edge.bridge.modbus.api.Stopbit; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.controller.test.ControllerTest; + +public class ControllerApiModbusRtuReadWriteImplTest { + + private static final String CTRL_ID = "ctrl0"; + + @Test + public void test() throws Exception { + new ControllerTest(new ControllerApiModbusRtuReadWriteImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEnabled(false) // do not actually start server + .setParity(Parity.NONE).setStopbit(Stopbit.ONE).setBaudrate(9600) // + .setComponentIds() // + .setMaxConcurrentConnections(5) // + .setPortName("/dev/ttyUSB0") // + .build()) // + .next(new TestCase()) // + ; + } +} diff --git a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/rtu/MyConfig.java b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/rtu/MyConfig.java new file mode 100644 index 00000000000..6f28ca0db52 --- /dev/null +++ b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/rtu/MyConfig.java @@ -0,0 +1,147 @@ +package io.openems.edge.controller.api.modbus.readwrite.rtu; + +import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.utils.ConfigUtils; +import io.openems.edge.bridge.modbus.api.Parity; +import io.openems.edge.bridge.modbus.api.Stopbit; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + private String id; + private boolean enabled; + private String portName; + private String[] componentIds; + private int baudrate; + private int databits; + private Stopbit stopbit; + private Parity parity; + private int apiTimeout; + private int maxConcurrentConnections; + + private Builder() { + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public Builder setPortName(String portName) { + this.portName = portName; + return this; + } + + public Builder setComponentIds(String... componentIds) { + this.componentIds = componentIds; + return this; + } + + public Builder setMaxConcurrentConnections(int maxConcurrentConnections) { + this.maxConcurrentConnections = maxConcurrentConnections; + return this; + } + + public Builder setBaudrate(int baudrate) { + this.baudrate = baudrate; + return this; + } + + public Builder setDatabits(int databits) { + this.databits = databits; + return this; + } + + public Builder setStopbit(Stopbit stopbit) { + this.stopbit = stopbit; + return this; + } + + public Builder setParity(Parity parity) { + this.parity = parity; + return this; + } + + public Builder setApiTimeout(int apiTimeout) { + this.apiTimeout = apiTimeout; + 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 boolean enabled() { + return this.builder.enabled; + } + + @Override + public String portName() { + return this.builder.portName; + } + + @Override + public int apiTimeout() { + return this.builder.apiTimeout; + } + + @Override + public int baudRate() { + return this.builder.baudrate; + } + + @Override + public int databits() { + return this.builder.baudrate; + } + + @Override + public Stopbit stopbits() { + return this.builder.stopbit; + } + + @Override + public Parity parity() { + return this.builder.parity; + } + + @Override + public String[] component_ids() { + return this.builder.componentIds; + } + + @Override + public int maxConcurrentConnections() { + return this.builder.maxConcurrentConnections; + } + + @Override + public String Component_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), false, this.component_ids()); + } + +} \ No newline at end of file diff --git a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImplTest.java b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/tcp/ControllerApiModbusTcpReadWriteImplTest.java similarity index 72% rename from io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImplTest.java rename to io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/tcp/ControllerApiModbusTcpReadWriteImplTest.java index f87124a5295..7ca3f80aaaa 100644 --- a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImplTest.java +++ b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/tcp/ControllerApiModbusTcpReadWriteImplTest.java @@ -1,8 +1,8 @@ -package io.openems.edge.controller.api.modbus.readwrite; +package io.openems.edge.controller.api.modbus.readwrite.tcp; import static io.openems.edge.controller.api.modbus.AbstractModbusTcpApi.DEFAULT_PORT; -import static io.openems.edge.controller.api.modbus.readwrite.ControllerApiModbusTcpReadWriteImpl.getChannelNameCamel; -import static io.openems.edge.controller.api.modbus.readwrite.ControllerApiModbusTcpReadWriteImpl.getChannelNameUpper; +import static io.openems.edge.controller.api.modbus.readwrite.tcp.ControllerApiModbusTcpReadWriteImpl.getChannelNameCamel; +import static io.openems.edge.controller.api.modbus.readwrite.tcp.ControllerApiModbusTcpReadWriteImpl.getChannelNameUpper; import static io.openems.edge.ess.api.ManagedSymmetricEss.ChannelId.SET_ACTIVE_POWER_EQUALS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -53,13 +53,17 @@ public void testAddFalseComponents() throws Exception { @Test public void testGetChannelNameUpper() { - assertEquals("ESS0_SET_ACTIVE_POWER_EQUALS", getChannelNameUpper("ess0", SET_ACTIVE_POWER_EQUALS)); - assertEquals("ESS0_SET_ACTIVE_POWER_EQUALS", getChannelNameUpper("Ess0", SET_ACTIVE_POWER_EQUALS)); + assertEquals("ESS0_SET_ACTIVE_POWER_EQUALS", + getChannelNameUpper("ess0", SET_ACTIVE_POWER_EQUALS)); + assertEquals("ESS0_SET_ACTIVE_POWER_EQUALS", + getChannelNameUpper("Ess0", SET_ACTIVE_POWER_EQUALS)); } @Test public void testGetChannelNameCamel() { - assertEquals("Ess0SetActivePowerEquals", getChannelNameCamel("ess0", SET_ACTIVE_POWER_EQUALS)); - assertEquals("Ess0SetActivePowerEquals", getChannelNameCamel("Ess0", SET_ACTIVE_POWER_EQUALS)); + assertEquals("Ess0SetActivePowerEquals", + getChannelNameCamel("ess0", SET_ACTIVE_POWER_EQUALS)); + assertEquals("Ess0SetActivePowerEquals", + getChannelNameCamel("Ess0", SET_ACTIVE_POWER_EQUALS)); } } diff --git a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/MyConfig.java b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/tcp/MyConfig.java similarity index 97% rename from io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/MyConfig.java rename to io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/tcp/MyConfig.java index 1b761e3d72a..6a6a40a630a 100644 --- a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/MyConfig.java +++ b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/tcp/MyConfig.java @@ -1,4 +1,4 @@ -package io.openems.edge.controller.api.modbus.readwrite; +package io.openems.edge.controller.api.modbus.readwrite.tcp; import io.openems.common.test.AbstractComponentConfig; import io.openems.common.utils.ConfigUtils; diff --git a/io.openems.edge.controller.ess.emergencycapacityreserve/src/io/openems/edge/controller/ess/emergencycapacityreserve/statemachine/Context.java b/io.openems.edge.controller.ess.emergencycapacityreserve/src/io/openems/edge/controller/ess/emergencycapacityreserve/statemachine/Context.java index 68f2031ba43..588d84b1bdb 100644 --- a/io.openems.edge.controller.ess.emergencycapacityreserve/src/io/openems/edge/controller/ess/emergencycapacityreserve/statemachine/Context.java +++ b/io.openems.edge.controller.ess.emergencycapacityreserve/src/io/openems/edge/controller/ess/emergencycapacityreserve/statemachine/Context.java @@ -24,6 +24,7 @@ public class Context extends AbstractContext= reserveSoc + 1 || soc == 100) { diff --git a/io.openems.edge.controller.ess.emergencycapacityreserve/src/io/openems/edge/controller/ess/emergencycapacityreserve/statemachine/StateMachine.java b/io.openems.edge.controller.ess.emergencycapacityreserve/src/io/openems/edge/controller/ess/emergencycapacityreserve/statemachine/StateMachine.java index ad0286d4054..c42fbe6677d 100644 --- a/io.openems.edge.controller.ess.emergencycapacityreserve/src/io/openems/edge/controller/ess/emergencycapacityreserve/statemachine/StateMachine.java +++ b/io.openems.edge.controller.ess.emergencycapacityreserve/src/io/openems/edge/controller/ess/emergencycapacityreserve/statemachine/StateMachine.java @@ -82,6 +82,7 @@ public void run(Context context) throws OpenemsNamedException { this.lastActiveState = this.getPreviousState(); } context.setLastActiveState(this.lastActiveState); + context.setPreviousState(this.getPreviousState()); super.run(context); } diff --git a/io.openems.edge.controller.ess.emergencycapacityreserve/test/io/openems/edge/controller/ess/emergencycapacityreserve/ControllerEssEmergencyCapacityReserveImplTest.java b/io.openems.edge.controller.ess.emergencycapacityreserve/test/io/openems/edge/controller/ess/emergencycapacityreserve/ControllerEssEmergencyCapacityReserveImplTest.java index 2ab88d2be77..30c0e7e27d6 100644 --- a/io.openems.edge.controller.ess.emergencycapacityreserve/test/io/openems/edge/controller/ess/emergencycapacityreserve/ControllerEssEmergencyCapacityReserveImplTest.java +++ b/io.openems.edge.controller.ess.emergencycapacityreserve/test/io/openems/edge/controller/ess/emergencycapacityreserve/ControllerEssEmergencyCapacityReserveImplTest.java @@ -606,20 +606,20 @@ public void testGridChargingOn() throws Exception { )// .next(new TestCase()// .input("ess0", SOC, 18).output(STATE_MACHINE, State.FORCE_CHARGE_GRID) // - .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9100)// - .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9100) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -1000)// + .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, -1000) // )// .next(new TestCase()// .input("ess0", SOC, 18)// .output(STATE_MACHINE, State.FORCE_CHARGE_GRID) // - .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 9000)// - .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 9000) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -1000)// + .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, -1000) // ) // From Below .next(new TestCase()// .input("ess0", SOC, 15).output(STATE_MACHINE, State.FORCE_CHARGE_GRID) // - .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, 8900)// - .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, 8900) // + .output("ess0", SET_ACTIVE_POWER_LESS_OR_EQUALS, -1100)// + .output(DEBUG_SET_ACTIVE_POWER_LESS_OR_EQUALS, -1100) // ) // let ramp run its course .next(new TestCase()// diff --git a/io.openems.edge.core/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.core/.settings/org.eclipse.core.resources.prefs index 2b7c020688d..345c44953dd 100644 --- a/io.openems.edge.core/.settings/org.eclipse.core.resources.prefs +++ b/io.openems.edge.core/.settings/org.eclipse.core.resources.prefs @@ -1,4 +1,6 @@ eclipse.preferences.version=1 encoding//src/io/openems/edge/core/appmanager/dependency/translation_de.properties=UTF-8 encoding//src/io/openems/edge/core/appmanager/translation_de.properties=UTF-8 +encoding//src/io/openems/edge/core/appmanager/translation_en.properties=UTF-8 +encoding//src/io/openems/edge/core/appmanager/validator/translation_de.properties=UTF-8 encoding/=UTF-8 diff --git a/io.openems.edge.core/bnd.bnd b/io.openems.edge.core/bnd.bnd index 58ebb64e861..c5001d1d1ae 100644 --- a/io.openems.edge.core/bnd.bnd +++ b/io.openems.edge.core/bnd.bnd @@ -5,6 +5,7 @@ Bundle-Version: 1.0.0.${tstamp} -buildpath: \ ${buildpath},\ + com.fazecast.jSerialComm,\ io.openems.common,\ io.openems.edge.common,\ io.openems.edge.controller.api,\ @@ -24,4 +25,4 @@ Bundle-Version: 1.0.0.${tstamp} -testpath: \ ${testpath},\ io.openems.wrapper.fastexcel,\ - io.openems.wrapper.opczip + io.openems.wrapper.opczip,\ \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusApiProps.java b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusApiProps.java new file mode 100644 index 00000000000..a75928ed63a --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusApiProps.java @@ -0,0 +1,188 @@ +package io.openems.edge.app.api; + +import static io.openems.edge.app.common.props.CommonProps.defaultDef; +import static io.openems.edge.core.appmanager.formly.enums.InputType.NUMBER; + +import java.util.ArrayList; +import java.util.List; + +import com.fazecast.jSerialComm.SerialPort; +import com.google.gson.JsonNull; +import com.google.gson.JsonPrimitive; + +import io.openems.common.session.Role; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.common.props.ComponentProps; +import io.openems.edge.common.modbusslave.ModbusSlave; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.ComponentManagerSupplier; +import io.openems.edge.core.appmanager.ComponentUtilSupplier; +import io.openems.edge.core.appmanager.Nameable; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.Type.Parameter.BundleProvider; +import io.openems.edge.core.appmanager.formly.Exp; +import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; +import io.openems.edge.core.appmanager.formly.builder.ReorderArrayBuilder.SelectOptionExpressions; + +public final class ModbusApiProps { + + /** + * Creates a {@link AppDef} to select {@link ModbusSlave} Components for a + * ModbusApi. + * + * @param the type of the {@link OpenemsApp} + * @return the {@link AppDef} + */ + public static AppDef pickModbusIds() { + return AppDef.copyOfGeneric(ComponentProps.pickOrderedArrayIds(ModbusSlave.class, component -> { + if ("_meta".equals(component.id())) { + return false; + } + return true; + }, (app, property, l, parameter, component) -> { + if ("_sum".equals(component.id())) { + final var lockedExpression = Exp.currentModelValue(property).asArray() // + .elementAt(0).equal(Exp.staticValue(component.id())); + return new SelectOptionExpressions(lockedExpression); + } + return null; + }, List.of((app, property, language, parameter) -> { + return JsonFormlyUtil.buildText() // + .setText(TranslationUtil.getTranslation(parameter.bundle(), "App.Api.Modbus.changeComponentHint")); + })), def -> def // + .setTranslatedLabel("component.id.plural") // + ); + } + + /** + * Creates a {@link AppDef} to select {@link ModbusSlave} Components for a + * ModbusApi. + * + * @param the type of the {@link OpenemsApp} + * @return the {@link AppDef} + */ + public static AppDef apiTimeout() { + return AppDef.copyOfGeneric(defaultDef(), def -> def // + .setTranslatedLabel("App.Api.apiTimeout.label") // + .setTranslatedDescription("App.Api.apiTimeout.description") // + .setDefaultValue(60) // + .setField(JsonFormlyUtil::buildInputFromNameable, (app, property, l, parameter, field) -> { + field.setInputType(NUMBER) // + .setMin(0); + })); // + } + + /** + * Creates a {@link AppDef} to select Port Name for ModbusRTU Api. + * + * @param the type of the {@link OpenemsApp} + * @return the {@link AppDef} + */ + public static AppDef portName() { + return AppDef.copyOfGeneric(defaultDef(), def -> def // + .setTranslatedLabel("App.Api.ModbusRtu.portName.label") // + .setTranslatedDescription("App.Api.ModbusRtu.portName.description") // + .setDefaultValue((app, property, l, parameter) -> { + SerialPort[] ports = SerialPort.getCommPorts(); + if (ports.length > 0) { + return new JsonPrimitive(ports[0].getSystemPortName()); + } + return JsonNull.INSTANCE; + }).setField(JsonFormlyUtil::buildSelectFromNameable, (app, property, l, parameter, field) -> { + SerialPort[] ports = SerialPort.getCommPorts(); + var portNames = new ArrayList(); + for (var port : ports) { + portNames.add(port.getSystemPortName()); + } + field.setOptions(portNames); + })); // + } + + /** + * Creates a {@link AppDef} to select baudrate for ModbusRTU Api. + * + * @param the type of the {@link OpenemsApp} + * @return the {@link AppDef} + */ + public static AppDef baudrate() { + return AppDef.copyOfGeneric(defaultDef(), def -> def // + .setTranslatedLabel("App.Api.ModbusRtu.baudrate.label") // + .setTranslatedDescription("App.Api.ModbusRtu.baudrate.description") // + .setDefaultValue(9600) // + .setField(JsonFormlyUtil::buildInputFromNameable, (app, property, l, parameter, field) -> { + field.setInputType(NUMBER) // + .setMin(0); + })); // + } + + /** + * Creates a {@link AppDef} to select Databits for ModbusRTU Api. + * + * @param the type of the {@link OpenemsApp} + * @return the {@link AppDef} + */ + public static AppDef databits() { + return AppDef.copyOfGeneric(defaultDef(), def -> def // + .setTranslatedLabel("App.Api.ModbusRtu.databits.label") // + .setTranslatedDescription("App.Api.ModbusRtu.databits.description") // + .setDefaultValue(8)); + } + + /** + * Creates a {@link AppDef} to select Stopbits for ModbusRTU Api. + * + * @param the type of the {@link OpenemsApp} + * @return the {@link AppDef} + */ + public static AppDef stopbits() { + return AppDef.copyOfGeneric(defaultDef(), def -> def // + .setTranslatedLabel("App.Api.ModbusRtu.stopbits.label") // + .setTranslatedDescription("App.Api.ModbusRtu.stopbits.description") // + .setDefaultValue("ONE") // + .setField(JsonFormlyUtil::buildSelectFromNameable, (app, prop, l, params, field) -> { + field.setOptions(List.of("ONE", "ONE_POINT_FIVE", "TWO")); + })); + } + + /** + * Creates a {@link AppDef} to select Parity for ModbusRTU Api. + * + * @param the type of the {@link OpenemsApp} + * @return the {@link AppDef} + */ + public static AppDef parity() { + return AppDef.copyOfGeneric(defaultDef(), def -> def // + .setTranslatedLabel("App.Api.ModbusRtu.parity.label") // + .setTranslatedDescription("App.Api.ModbusRtu.parity.description") // + .setDefaultValue("NONE") // + .setField(JsonFormlyUtil::buildSelectFromNameable, (app, prop, l, params, field) -> { + field.setOptions(List.of("NONE", "ODD", "EVEN", "MARK", "SPACE")); + })); + } + + /** + * Creates a {@link AppDef} to select Component Ids for ModbusApi. + * + * @param the type of the {@link OpenemsApp} + * @param componentName the component name + * @return the {@link AppDef} + */ + public static AppDef componentIds( + Nameable componentName) { + return AppDef.copyOfGeneric(ModbusApiProps.pickModbusIds(), def -> def // + .setDefaultValue((app, property, l, parameter) -> { + final var jsonArrayBuilder = JsonUtils.buildJsonArray() // + .add("_sum"); + + return jsonArrayBuilder.build(); + }) // + .bidirectional(componentName, "component.ids", ComponentManagerSupplier::getComponentManager) // + .appendIsAllowedToSee(AppDef.ofLeastRole(Role.ADMIN))); // + + } + + private ModbusApiProps() { + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusRtuApiReadOnly.java b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusRtuApiReadOnly.java new file mode 100644 index 00000000000..35a002711ad --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusRtuApiReadOnly.java @@ -0,0 +1,191 @@ +package io.openems.edge.app.api; + +import static io.openems.edge.app.common.props.CommonProps.alias; + +import java.util.Map; +import java.util.function.Function; + +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.Reference; + +import com.google.common.collect.Lists; +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.api.ModbusRtuApiReadOnly.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.Nameable; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; +import io.openems.edge.core.appmanager.dependency.aggregatetask.SchedulerByCentralOrderConfiguration.SchedulerComponent; + +/** + * Describes a App for ReadOnly Modbus/RTU Api. + * + *
+  {
+    "appId":"App.Api.ModbusRtu.ReadOnly",
+    "alias":"Modbus/RTU-Api Read-Only",
+    "instanceId": UUID,
+    "image": base64,
+    "properties":{
+    	"ACTIVE": true,
+    	"CONTROLLER_ID": "ctrlApiModbusRtu0"
+    },
+    "appDescriptor": {
+    	"websiteUrl": {@link AppDescriptor#getWebsiteUrl()}
+    }
+  }
+ * 
+ */ +@Component(name = "App.Api.ModbusRtu.ReadOnly") +public class ModbusRtuApiReadOnly extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public static enum Property implements Type, Nameable { + // Component-IDs + CONTROLLER_ID(AppDef.componentId("ctrlApiModbusRtu0")), // + // Properties + ALIAS(alias()), // + API_TIMEOUT(ModbusApiProps.apiTimeout() // + .setRequired(true)), // + COMPONENT_IDS(ModbusApiProps.componentIds(CONTROLLER_ID) // + .setRequired(true)), // + PORT_NAME(ModbusApiProps.portName() // + .setRequired(true)), // + BAUDRATE(ModbusApiProps.baudrate() // + .setRequired(true)), // + DATABITS(ModbusApiProps.databits() // + .setRequired(true)), + STOPBITS(ModbusApiProps.stopbits() // + .setRequired(true)), // + PARITY(ModbusApiProps.parity() // + .setRequired(true)); // + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public ModbusRtuApiReadOnly(@Reference ComponentManager componentManager, ComponentContext context, + @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + super(componentManager, context, cm, componentUtil); + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.API }; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE; + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + final var portName = this.getString(p, Property.PORT_NAME); + final var alias = this.getString(p, Property.ALIAS); + final var controllerId = this.getId(t, p, Property.CONTROLLER_ID); + final var apiTimeout = this.getInt(p, Property.API_TIMEOUT); + final var controllerIds = this.getJsonArray(p, Property.COMPONENT_IDS); + final var baudrate = this.getInt(p, Property.BAUDRATE); + final var databits = this.getInt(p, Property.DATABITS); + final var stopbits = this.getString(p, Property.STOPBITS); + final var parity = this.getString(p, Property.PARITY); + + // remove self if selected + for (var i = 0; i < controllerIds.size(); i++) { + if (controllerIds.get(i).getAsString().equals(controllerId)) { + controllerIds.remove(i); + break; + } + } + + final var components = Lists.newArrayList(// + new EdgeConfig.Component(controllerId, alias, "Controller.Api.ModbusRtu.ReadOnly", + JsonUtils.buildJsonObject() // + .addProperty("apiTimeout", apiTimeout) // + .add("component.ids", controllerIds) // + .addProperty("portName", portName) // + .addProperty("baudRate", baudrate) // + .addProperty("databits", databits) // + .addProperty("stopbits", stopbits) // + .addProperty("parity", parity) // + .build()) // + ); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.schedulerByCentralOrder(// + new SchedulerComponent(controllerId, "Controller.Api.ModbusRtu.ReadOnly", this.getAppId()))) // + .build(); + }; + } + + @Override + protected ModbusRtuApiReadOnly getApp() { + return this; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create()// + .setCanDelete(Role.ADMIN)// + .setCanSee(Role.ADMIN)// + .build(); + } + +} \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusRtuApiReadWrite.java b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusRtuApiReadWrite.java new file mode 100644 index 00000000000..12e4fa9f6b2 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusRtuApiReadWrite.java @@ -0,0 +1,197 @@ +package io.openems.edge.app.api; + +import static io.openems.edge.app.common.props.CommonProps.alias; + +import java.util.Map; +import java.util.function.Function; + +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.Reference; + +import com.google.common.collect.Lists; +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.api.ModbusRtuApiReadWrite.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; +import io.openems.edge.core.appmanager.dependency.aggregatetask.SchedulerByCentralOrderConfiguration.SchedulerComponent; + +/** + * Describes a App for ReadWrite Modbus/Rtu Api. + * + *
+  {
+    "appId":"App.Api.ModbusRtu.ReadWrite",
+    "alias":"Modbus/Rtu-Api Read-Write",
+    "instanceId": UUID,
+    "image": base64,
+    "properties":{
+    	"CONTROLLER_ID": "ctrlApiModbusRtu0",
+    	"API_TIMEOUT": 60,
+    	"COMPONENT_IDS": ["_sum", ...]
+    },
+    "dependencies": [
+    	{
+        	"key": "READ_ONLY",
+        	"instanceId": UUID
+    	}
+    ],
+    "appDescriptor": {
+    	"websiteUrl": {@link AppDescriptor#getWebsiteUrl()}
+    }
+  }
+ * 
+ */ +@Component(name = "App.Api.ModbusRtu.ReadWrite") +public class ModbusRtuApiReadWrite extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public static enum Property implements Type { + // Component-IDs + CONTROLLER_ID(AppDef.componentId("ctrlApiModbusRtu0")), // + // Properties + ALIAS(alias()), // + API_TIMEOUT(ModbusApiProps.apiTimeout() // + .setRequired(true)), // + COMPONENT_IDS(ModbusApiProps.componentIds(CONTROLLER_ID) // + .setRequired(true)), // + PORT_NAME(ModbusApiProps.portName() // + .setRequired(true)), // + BAUDRATE(ModbusApiProps.baudrate() // + .setRequired(true)), // + DATABITS(ModbusApiProps.databits() // + .setRequired(true)), + STOPBITS(ModbusApiProps.stopbits() // + .setRequired(true)), // + PARITY(ModbusApiProps.parity() // + .setRequired(true)); // + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public ModbusRtuApiReadWrite(@Reference ComponentManager componentManager, ComponentContext context, + @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + super(componentManager, context, cm, componentUtil); + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.API }; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE; + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + final var portName = this.getString(p, Property.PORT_NAME); + final var controllerId = this.getId(t, p, Property.CONTROLLER_ID); + final var apiTimeout = this.getInt(p, Property.API_TIMEOUT); + final var controllerIds = this.getJsonArray(p, Property.COMPONENT_IDS); + final var baudrate = this.getInt(p, Property.BAUDRATE); + final var databits = this.getInt(p, Property.DATABITS); + final var stopbits = this.getString(p, Property.STOPBITS); + final var parity = this.getString(p, Property.PARITY); + + // remove self if selected + for (var i = 0; i < controllerIds.size(); i++) { + if (controllerIds.get(i).getAsString().equals(controllerId)) { + controllerIds.remove(i); + break; + } + } + + final var components = Lists.newArrayList(// + new EdgeConfig.Component(controllerId, this.getName(l), "Controller.Api.ModbusRtu.ReadWrite", + JsonUtils.buildJsonObject() // + .addProperty("apiTimeout", apiTimeout) // + .add("component.ids", controllerIds) // + .addProperty("portName", portName) // + .addProperty("baudRate", baudrate) // + .addProperty("databits", databits) // + .addProperty("stopbits", stopbits) // + .addProperty("parity", parity) // + .build()) // + ); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.schedulerByCentralOrder(// + new SchedulerComponent(controllerId, "Controller.Api.ModbusRtu.ReadWrite", + this.getAppId()))) // + .build(); + }; + } + + @Override + protected ModbusRtuApiReadWrite getApp() { + return this; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create()// + .setCanDelete(Role.ADMIN)// + .setCanSee(Role.ADMIN)// + .build(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiProps.java b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiProps.java deleted file mode 100644 index e4266469c4e..00000000000 --- a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiProps.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.openems.edge.app.api; - -import java.util.List; - -import io.openems.edge.app.common.props.ComponentProps; -import io.openems.edge.common.modbusslave.ModbusSlave; -import io.openems.edge.core.appmanager.AppDef; -import io.openems.edge.core.appmanager.ComponentUtilSupplier; -import io.openems.edge.core.appmanager.Nameable; -import io.openems.edge.core.appmanager.OpenemsApp; -import io.openems.edge.core.appmanager.TranslationUtil; -import io.openems.edge.core.appmanager.Type.Parameter.BundleProvider; -import io.openems.edge.core.appmanager.formly.Exp; -import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; -import io.openems.edge.core.appmanager.formly.builder.ReorderArrayBuilder.SelectOptionExpressions; - -public final class ModbusTcpApiProps { - - /** - * Creates a {@link AppDef} to select {@link ModbusSlave} Components for a - * ModbusTcpApi. - * - * @param the type of the {@link OpenemsApp} - * @return the {@link AppDef} - */ - public static AppDef pickModbusIds() { - return AppDef.copyOfGeneric(ComponentProps.pickOrderedArrayIds(ModbusSlave.class, component -> { - if ("_meta".equals(component.id())) { - return false; - } - return true; - }, (app, property, l, parameter, component) -> { - if ("_sum".equals(component.id())) { - final var lockedExpression = Exp.currentModelValue(property).asArray() // - .elementAt(0).equal(Exp.staticValue(component.id())); - return new SelectOptionExpressions(lockedExpression); - } - return null; - }, List.of((app, property, language, parameter) -> { - return JsonFormlyUtil.buildText() // - .setText(TranslationUtil.getTranslation(parameter.bundle(), - "App.Api.ModbusTcp.changeComponentHint")); - })), def -> def // - .setTranslatedLabel("component.id.plural") // - ); - } - - private ModbusTcpApiProps() { - } - -} diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadOnly.java b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadOnly.java index 4298f4ebf23..99fbcba7748 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadOnly.java +++ b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadOnly.java @@ -19,7 +19,6 @@ import io.openems.common.function.ThrowingTriFunction; import io.openems.common.oem.OpenemsEdgeOem; import io.openems.common.session.Language; -import io.openems.common.session.Role; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.api.ModbusTcpApiReadOnly.Property; @@ -29,7 +28,6 @@ import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDef; import io.openems.edge.core.appmanager.AppDescriptor; -import io.openems.edge.core.appmanager.ComponentManagerSupplier; import io.openems.edge.core.appmanager.ComponentUtil; import io.openems.edge.core.appmanager.ConfigurationTarget; import io.openems.edge.core.appmanager.Nameable; @@ -74,14 +72,8 @@ public static enum Property implements Type def // - .setDefaultValue((app, property, l, parameter) -> { - return JsonUtils.buildJsonArray() // - .add("_sum") // - .build(); - }) // - .bidirectional(CONTROLLER_ID, "component.ids", ComponentManagerSupplier::getComponentManager) // - .appendIsAllowedToSee(AppDef.ofLeastRole(Role.ADMIN)))), // + COMPONENT_IDS(ModbusApiProps.componentIds(CONTROLLER_ID) // + .setRequired(true)) // ; private AppDef def; diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadWrite.java b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadWrite.java index 4aca80fbfd8..3e5fbe09221 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadWrite.java +++ b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadWrite.java @@ -1,7 +1,6 @@ package io.openems.edge.app.api; -import static io.openems.edge.app.common.props.CommonProps.defaultDef; -import static io.openems.edge.core.appmanager.formly.enums.InputType.NUMBER; +import static io.openems.edge.app.common.props.CommonProps.alias; import java.util.Map; import java.util.function.Function; @@ -18,7 +17,6 @@ import io.openems.common.function.ThrowingTriFunction; import io.openems.common.oem.OpenemsEdgeOem; import io.openems.common.session.Language; -import io.openems.common.session.Role; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.api.ModbusTcpApiReadWrite.Property; @@ -28,7 +26,6 @@ import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDef; import io.openems.edge.core.appmanager.AppDescriptor; -import io.openems.edge.core.appmanager.ComponentManagerSupplier; import io.openems.edge.core.appmanager.ComponentUtil; import io.openems.edge.core.appmanager.ConfigurationTarget; import io.openems.edge.core.appmanager.OpenemsApp; @@ -39,7 +36,6 @@ import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.dependency.aggregatetask.SchedulerByCentralOrderConfiguration.SchedulerComponent; -import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; /** * Describes a App for ReadWrite Modbus/TCP Api. @@ -75,30 +71,11 @@ public static enum Property implements Type def // - .setTranslatedLabel("App.Api.apiTimeout.label") // - .setTranslatedDescription("App.Api.apiTimeout.description") // - .setDefaultValue(60) // - .setRequired(true) // - .setField(JsonFormlyUtil::buildInput, (app, property, l, parameter, field) -> { - field.setInputType(NUMBER) // - .setMin(0); - }) // - )), // - COMPONENT_IDS(AppDef.copyOfGeneric(ModbusTcpApiProps.pickModbusIds(), def -> def // - .setDefaultValue((app, property, l, parameter) -> { - final var jsonArrayBuilder = JsonUtils.buildJsonArray() // - .add("_sum"); - - // add ess ids - app.getComponentUtil().getEnabledComponentsOfStartingId("ess").stream() // - .sorted((o1, o2) -> o1.id().compareTo(o2.id())) // - .forEach(ess -> jsonArrayBuilder.add(ess.id())); - - return jsonArrayBuilder.build(); - }) // - .bidirectional(CONTROLLER_ID, "component.ids", ComponentManagerSupplier::getComponentManager) // - .appendIsAllowedToSee(AppDef.ofLeastRole(Role.ADMIN)))), // + ALIAS(alias()), // + API_TIMEOUT(ModbusApiProps.apiTimeout() // + .setRequired(true)), // + COMPONENT_IDS(ModbusApiProps.componentIds(CONTROLLER_ID) // + .setRequired(true)) // ; private final AppDef def; diff --git a/io.openems.edge.core/src/io/openems/edge/app/core/AppMeta.java b/io.openems.edge.core/src/io/openems/edge/app/core/AppMeta.java new file mode 100644 index 00000000000..0031aef77b8 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/core/AppMeta.java @@ -0,0 +1,178 @@ +package io.openems.edge.app.core; + +import static io.openems.edge.app.common.props.CommonProps.alias; +import static io.openems.edge.app.common.props.CommonProps.defaultDef; + +import java.util.ArrayList; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +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.Reference; + +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.types.CurrencyConfig; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.core.AppMeta.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentManagerSupplier; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.OpenemsAppStatus; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; +import io.openems.edge.core.appmanager.flag.Flag; +import io.openems.edge.core.appmanager.flag.Flags; +import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; +import io.openems.edge.core.appmanager.formly.enums.DisplayType; + +@Component(name = "App.Core.Meta") +public class AppMeta extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public enum Property implements Type { + ALIAS(AppDef.copyOfGeneric(alias())), // + + CURRENCY(AppDef.copyOfGeneric(defaultDef(), def -> def// + .setTranslatedLabelWithAppPrefix(".currency.label") + .setField(JsonFormlyUtil::buildSelectFromNameable, (app, property, l, parameter, field) -> { + field.setOptions(Stream.of(CurrencyConfig.values()).map(Enum::name).toList()); + }) // + .bidirectional("_meta", "currency", ComponentManagerSupplier::getComponentManager))), // + IS_ESS_CHARGE_FROM_GRID_ALLOWED(AppDef.copyOfGeneric(defaultDef(), def -> def// + .setTranslatedLabelWithAppPrefix(".gridCharge.label") // + .setField(JsonFormlyUtil::buildFieldGroupFromNameable, (app, property, l, parameter, field) -> { + var bundle = parameter.bundle(); + field.setPopupInput(property, DisplayType.BOOLEAN); + field.setFieldGroup(JsonUtils.buildJsonArray() // + .add(JsonFormlyUtil.buildText() // + .setText(TranslationUtil.getTranslation(bundle, "App.Core.Meta.gridCharge.description")) + .build()) + .add(JsonFormlyUtil.buildCheckboxFromNameable(property) // + .setLabel(TranslationUtil.getTranslation(bundle, "App.Core.Meta.gridCharge.label")) // + .build()) + .build()); + }) // + .bidirectional("_meta", "isEssChargeFromGridAllowed", ComponentManagerSupplier::getComponentManager))), // + ; + + private AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public AppMeta(// + @Reference final ComponentManager componentManager, // + final ComponentContext componentContext, // + @Reference final ConfigurationAdmin cm, // + @Reference final ComponentUtil componentUtil // + ) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + + final var currency = this.getEnum(p, CurrencyConfig.class, Property.CURRENCY); + final var isEssChargeFromGridAllowed = this.getBoolean(p, Property.IS_ESS_CHARGE_FROM_GRID_ALLOWED); + + final var components = new ArrayList(); + + components.add(new EdgeConfig.Component("_meta", "", "Core.Meta", // + JsonUtils.buildJsonObject() // + .addProperty("currency", currency) // + .addProperty("isEssChargeFromGridAllowed", isEssChargeFromGridAllowed) // + .build())); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); + }; + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .build(); + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE; + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.CORE }; + } + + @Override + protected AppMeta getApp() { + return this; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create() // + .setCanDelete(Role.ADMIN) // TODO theoretically not even admin + .build(); + } + + @Override + public Flag[] flags() { + final var flags = new ArrayList<>(); + if (this.getStatus() == OpenemsAppStatus.BETA) { + flags.add(Flags.SHOW_AFTER_KEY_REDEEM); + } + flags.add(Flags.ALWAYS_INSTALLED); + return flags.toArray(Flag[]::new); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsApp.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsApp.java index aa5d3f2fe2f..6393780a8f1 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsApp.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsApp.java @@ -11,11 +11,13 @@ import java.util.TreeMap; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentConstants; import org.osgi.service.component.ComponentContext; +import com.google.common.base.CaseFormat; import com.google.common.collect.ImmutableMap; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -129,6 +131,60 @@ public AppConfiguration getAppConfiguration(ConfigurationTarget target, JsonObje return configuration; } + @Override + public String mapPropName(String prop, String componentId, OpenemsAppInstance instance) { + var enumMap = this.convertToMap(new ArrayList<>(), instance.properties); + var mappedPropName = this.mapPropNameWithMap(enumMap, prop, componentId); + return this.getPropertyByName(mappedPropName) == null ? null : mappedPropName; + } + + /** + * Convert JsonObject with Properties to Map. + * + * @param componentId id of the component + * @param prop the propertyname + * @param map map of the instance + * @return a typed {@link Map} of Properties + */ + private String mapPropNameWithMap(Map map, String prop, String componentId) { + return this.transformCase(prop); + } + + private String transformCase(String prop) { + var parsedPropName = prop; + if (prop.contains(".")) { + parsedPropName = pointedCaseToUpperUnderscore(prop); + } else { + parsedPropName = lowerCamelToUpperUnderscore(prop); + } + return parsedPropName; + } + + private static String pointedCaseToUpperUnderscore(String str) { + return str.replace('.', '_').toUpperCase(); + } + + private static boolean isLowerCamelCase(String str) { + if (str == null || str.length() == 0) { + return false; + } + boolean isFirstCharUpperCaseLetter = Character.isUpperCase(str.charAt(0)) || !Character.isLetter(str.charAt(0)); + if (!isFirstCharUpperCaseLetter && str.length() > 1) { + return IntStream.range(1, str.length() - 1) + .noneMatch(i -> !Character.isLetter(str.charAt(i)) + || Character.isUpperCase(str.charAt(i)) && Character.isUpperCase(str.charAt(i + 1))) + && Character.isLetter(str.charAt(str.length() - 1)); + } + return !isFirstCharUpperCaseLetter; + } + + private static String lowerCamelToUpperUnderscore(String str) { + if (isLowerCamelCase(str)) { + return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, str); + } + return str; + } + @Override public String getAppId() { return this.componentContext.getProperties().get(ComponentConstants.COMPONENT_NAME).toString(); @@ -371,6 +427,11 @@ protected static final Component getComponentWithFactoryId(List compo return components.stream().filter(t -> t.getFactoryId().equals(factoryId)).findFirst().orElse(null); } + @Override + public boolean assertCanEdit(String prop, User user) { + return true; + } + @Override public ComponentManager getComponentManager() { return this.componentManager; diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsAppWithProps.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsAppWithProps.java index fa7043d8378..d75661808c9 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsAppWithProps.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsAppWithProps.java @@ -6,6 +6,7 @@ import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Stream; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -128,6 +129,16 @@ protected > E getEnum(// return this.getEnum(map, enumType, property, PROPERTY::def); } + @Override + public final String mapPropName(String prop, String componentId, OpenemsAppInstance instance) { + return Stream.of(this.propertyValues()).map(p -> p.def().getBidirectionalPropertyName()).filter(t -> { + if (t == null) { + return false; + } + return t.equals(prop); + }).findFirst().orElseGet(() -> super.mapPropName(prop, componentId, instance)); + } + protected boolean getBoolean(// final Map map, // final PROPERTY property, // @@ -269,6 +280,15 @@ public final T get() { } + @Override + public final boolean assertCanEdit(String propName, User user) { + final var prop = Stream.of(this.propertyValues())// + .filter(property -> property.name().equals(propName))// + .findFirst().orElseThrow(() -> new RuntimeException("Property " + propName + " does not exist")); + return prop.def().getIsAllowedToEdit().test(this.getApp(), prop, user.getLanguage(), + this.singletonParameter(user.getLanguage()).get(), user); + } + protected abstract APP getApp(); @Override diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppDef.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppDef.java index 777c07abda9..5064d50abbe 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppDef.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppDef.java @@ -169,6 +169,8 @@ public static interface FieldValuesConsumer { */ private FieldValuesSupplier description; + private String propertyName; + /** * Function to get the default value of the field (can be any JsonElement => * JsonArray, JsonPrimitiv(Number, String, Boolean, Character). @@ -360,6 +362,7 @@ PARAMETERO> AppDef copyOfGeneric(// def.bidirectionalValue = otherDef.bidirectionalValue; def.isAllowedToSee = otherDef.isAllowedToSee; def.isAllowedToEdit = otherDef.isAllowedToEdit; + def.propertyName = otherDef.propertyName; return def; } @@ -380,6 +383,10 @@ public AppDef setAllowedToSave(// return this; } + public AppDef setMinRole(final Role role) { + return this.appendIsAllowedToEdit(ofLeastRole(role)); + } + public final AppDef setTranslationBundleSupplier(// final Function bundleSupplier // ) { @@ -999,11 +1006,92 @@ public AppDef bidirectional(// final Function componentManagerFunction, // final Function mapper // ) { + return this.bidirectional(t -> { + final var a = t.get(propOfComponentId.name()); + return a == null ? null : a.getAsString(); + }, property, componentManagerFunction, mapper); + } + + /** + * Binds a property bidirectional. + * + *

+ * The property itself will not be stored in the app configuration only in the + * component. If the user doesn't provide the value of a property and there is a + * bidirectional binding for it it will be filled up with the value of the + * bidirectional binding. If there is no component id in the configuration or + * the component doesn't exist or the property of the value is null then null is + * returned inside the bidirectional function. + * + * @param componentId the componentId + * @param property the property + * @param componentManagerFunction the componentmanagerFunction + * @return this + */ + public AppDef bidirectional(// + final String componentId, // + final String property, // + final Function componentManagerFunction // + ) { + return this.bidirectional(componentId, property, componentManagerFunction, Function.identity()); + } + + /** + * Binds a property bidirectional. + * + *

+ * The property itself will not be stored in the app configuration only in the + * component. If the user doesn't provide the value of a property and there is a + * bidirectional binding for it it will be filled up with the value of the + * bidirectional binding. If there is no component id in the configuration or + * the component doesn't exist or the property of the value is null then null is + * returned inside the bidirectional function. + * + * @param componentId the componentId + * @param property the property + * @param componentManagerFunction the componentmanagerFunction + * @param mapper mapper + * @return this + */ + public AppDef bidirectional(// + final String componentId, // + final String property, // + final Function componentManagerFunction, // + final Function mapper // + ) { + return this.bidirectional(t -> componentId, property, componentManagerFunction, mapper); + } + + /** + * Binds a property bidirectional. + * + *

+ * The property itself will not be stored in the app configuration only in the + * component. If the user doesn't provide the value of a property and there is a + * bidirectional binding for it it will be filled up with the value of the + * bidirectional binding. If there is no component id in the configuration or + * the component doesn't exist or the property of the value is null then null is + * returned inside the bidirectional function. + * + * @param componentIdSupplier the componentId supplier + * @param property the property + * @param componentManagerFunction the componentmanagerFunction + * @param mapper mapper + * + * @return this + */ + public AppDef bidirectional(// + final Function componentIdSupplier, // + final String property, // + final Function componentManagerFunction, // + final Function mapper // + ) { + this.propertyName = property; this.bidirectionalValue = (app, prop, l, param, properties) -> { if (properties == null) { return null; } - final var componentId = properties.get(propOfComponentId.name()); + final var componentId = componentIdSupplier.apply(properties); if (componentId == null) { return null; } @@ -1016,12 +1104,19 @@ public AppDef bidirectional(// return JsonNull.INSTANCE; }); + final var p = componentManager.getComponentProperties(componentId); try { - final var component = componentManager.getComponent(componentId.getAsString()); - return Optional.ofNullable(component.getComponentContext().getProperties().get(property)) // + final var component = componentManager.getComponent(componentId); + + return Optional.ofNullable(p.get(property)) // .map(JsonUtils::getAsJsonElement) // .map(mapper) // - .orElseGet(defaultValueSupplier); + .orElseGet(() -> { + return Optional.ofNullable(component.getComponentContext().getProperties().get(property)) // + .map(JsonUtils::getAsJsonElement) // + .map(mapper) // + .orElseGet(defaultValueSupplier); + }); } catch (OpenemsNamedException e) { return defaultValueSupplier.get(); } @@ -1031,6 +1126,10 @@ public AppDef bidirectional(// return this.self(); } + public String getBidirectionalPropertyName() { + return this.propertyName; + } + /** * Creates a simple mapper if the input {@link JsonElement} is a number it gets * multiplied with the given multiplicator. diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerImpl.java index 25908265a55..0cb83d8f684 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerImpl.java @@ -9,10 +9,12 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; @@ -20,6 +22,7 @@ import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -61,6 +64,8 @@ import io.openems.edge.core.appmanager.dependency.AppManagerAppHelper; import io.openems.edge.core.appmanager.dependency.Dependency; import io.openems.edge.core.appmanager.dependency.UpdateValues; +import io.openems.edge.core.appmanager.flag.Flag; +import io.openems.edge.core.appmanager.flag.Flags; import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; import io.openems.edge.core.appmanager.jsonrpc.DeleteAppInstance; import io.openems.edge.core.appmanager.jsonrpc.GetApp; @@ -68,6 +73,8 @@ import io.openems.edge.core.appmanager.jsonrpc.GetAppDescriptor; import io.openems.edge.core.appmanager.jsonrpc.GetAppInstances; import io.openems.edge.core.appmanager.jsonrpc.GetApps; +import io.openems.edge.core.appmanager.jsonrpc.UpdateAppConfig; +import io.openems.edge.core.appmanager.jsonrpc.GetEstimatedConfiguration; import io.openems.edge.core.appmanager.jsonrpc.UpdateAppInstance; import io.openems.edge.core.appmanager.validator.Validator; @@ -90,8 +97,36 @@ public class AppManagerImpl extends AbstractOpenemsComponent implements AppManag @Reference private ConfigurationAdmin cm; - @Reference(policy = ReferencePolicy.DYNAMIC) - protected volatile List availableApps; + protected volatile List availableApps = new CopyOnWriteArrayList<>(); + + /** + * Binds newly available OpenemsApp. + * + * @param app the app + */ + @Reference(policy = ReferencePolicy.DYNAMIC, // + bind = "bindApp", unbind = "unbindApp", // + cardinality = ReferenceCardinality.MULTIPLE, // + policyOption = ReferencePolicyOption.GREEDY) + public void bindApp(OpenemsApp app) { + this.availableApps.add(app); + var alwaysInstalled = app.hasFlag(Flags.ALWAYS_INSTALLED); + if (alwaysInstalled) { + this.instantiatedApps + .add(new OpenemsAppInstance(app.getAppId(), "", UUID.randomUUID(), new JsonObject(), emptyList())); + } + } + + /** + * Unbinds no longer available apps. + * + * @param app the app + */ + public void unbindApp(OpenemsApp app) { + this.availableApps.remove(app); + var installedInstances = this.instantiatedApps.stream().filter(t -> t.appId == app.getAppId()).toList(); + this.instantiatedApps.removeAll(installedInstances); + } @Reference private ComponentServiceObjects csoAppManagerAppHelper; @@ -199,19 +234,24 @@ public final List getInstantiatedApps() { * @param apps that should be formatted * @return formatted apps string */ - private static String getJsonAppsString(List apps) { - return JsonUtils - .prettyToString(apps.stream().map(OpenemsAppInstance::toJsonObject).collect(JsonUtils.toJsonArray())); + private String getJsonAppsString(List apps) { + return JsonUtils.prettyToString(apps.stream() // + .filter(t -> !this.appIdIsAlwaysInstalled(t.appId)) // + .map(OpenemsAppInstance::toJsonObject) // + .collect(JsonUtils.toJsonArray())); } /** * Parses the configured apps to a List of {@link OpenemsAppInstance}s. * - * @param apps the app configuration from Config.json as {@link JsonArray} + * @param apps the app configuration from Config.json as + * {@link JsonArray} + * @param alwaysInstalledApps list of all apps that are always installed * @return List of {@link OpenemsAppInstance}s * @throws OpenemsNamedException on parse error */ - private static List parseInstantiatedApps(JsonArray apps) throws OpenemsNamedException { + private static List parseInstantiatedApps(JsonArray apps, List alwaysInstalledApps) + throws OpenemsNamedException { var errors = new ArrayList(); var result = new ArrayList(apps.size()); for (var appElement : apps) { @@ -237,6 +277,9 @@ private static List parseInstantiatedApps(JsonArray apps) th } result.add(new OpenemsAppInstance(appId, alias, instanceId, properties, dependecies)); } + var aip = alwaysInstalledApps.stream() + .map(t -> new OpenemsAppInstance(t, "", UUID.randomUUID(), new JsonObject(), emptyList())).toList(); + result.addAll(aip); if (!errors.isEmpty()) { throw new OpenemsException(errors.stream().collect(Collectors.joining("|"))); } @@ -253,8 +296,14 @@ private void applyConfig(Config config) { if (apps == null || apps.isBlank()) { apps = "[]"; // default to empty array } - var instApps = parseInstantiatedApps(JsonUtils.parseToJsonArray(apps)); + var alwaysInstalledApps = this.instantiatedApps.stream().filter(t -> { + return this.findAppById(t.appId)// + .map(a -> a.hasFlag(Flags.ALWAYS_INSTALLED))// + .orElse(false); + }).map(t -> t.appId).toList(); + + var instApps = parseInstantiatedApps(JsonUtils.parseToJsonArray(apps), alwaysInstalledApps); // always replace old apps with the new ones var currentApps = new ArrayList<>(this.instantiatedApps); @@ -829,6 +878,113 @@ public void buildJsonApiRoutes(JsonApiBuilder builder) { emptyList())); }); }, call -> this.handleAddAppInstanceRequest(call.get(EdgeKeys.USER_KEY), call.getRequest())); + + builder.handleRequest(new GetEstimatedConfiguration(), endpoint -> { + + endpoint.applyRequestBuilder(request -> { + request.addExample("Home 30", + new GetEstimatedConfiguration.Request("App.FENECON.Home.30", "FENECON Home 30", + JsonUtils.buildJsonObject() // + .addProperty("SAFETY_COUNTRY", "GERMANY") // + .addProperty("FEED_IN_TYPE", "EXTERNAL_LIMITATION") // + .addProperty("FEED_IN_SETTING", "QU_ENABLE_CURVE") // + .addProperty("HAS_MPPT_1", "true") // + .addProperty("ALIAS_MPPT_1", "String A and String B") // + .build())); + }); + + }, call -> { + final var request = call.getRequest(); + final var user = call.get(EdgeKeys.USER_KEY); + + final var app = this.findAppByIdOrError(request.appId()); + + final var configs = this.useAppManagerAppHelper(t -> { + return t.getInstallConfiguration(user, new OpenemsAppInstance(request.appId(), request.alias(), + UUID.randomUUID(), request.properties(), null), app); + }); + + return new GetEstimatedConfiguration.Response(configs); + }); + + builder.handleRequest(new UpdateAppConfig(), endpoint -> { + endpoint.setDescription(""" + Updates a AppInstance. + """.stripIndent()); + + endpoint.setGuards(EdgeGuards.roleIsAtleast(Role.OWNER)); + + }, call -> this.handleUpdateAppConfigRequest(call.get(EdgeKeys.USER_KEY), call.getRequest())); + } + + /** + * Find unique instanceId for given componentId}. + * + * @param componentId Id of the component the app configures + * @return the instanceId of the appInstance + * @throws OpenemsNamedException on error + */ + private OpenemsAppInstance findInstanceByComponentId(String componentId) throws OpenemsNamedException { + for (var appConfig : this.appConfigs()) { + var containsComponent = appConfig.getValue().getComponents().stream() + .anyMatch(t -> t.getId().equals(componentId)); + if (containsComponent) { + return appConfig.getKey(); + } + } + + return null; + } + + /** + * Handles {@link UpdateAppConfigRequest}. + * + * @param user the User + * @param request the {@link UpdateAppConfigRequest} Request + * @return the Future JSON-RPC Response + * @throws OpenemsNamedException on error + */ + public UpdateAppConfig.Response handleUpdateAppConfigRequest(User user, UpdateAppConfig.Request request) + throws OpenemsNamedException { + + final var appInstance = this.findInstanceByComponentId(request.componentId()); + + // update Component the old fashioned way if no app exists for the component + if (appInstance == null) { + return this.updateComponentDirectly(user, request); + } + final var app = this.findAppByIdOrError(appInstance.appId); + + final var requestProperties = request.properties().entrySet().stream() // + .map(entry -> Map.entry(app.mapPropName(entry.getKey(), request.componentId(), appInstance), + entry.getValue())) + .filter(entry -> entry.getKey() != null) + .collect(JsonUtils.toJsonObject(Entry::getKey, Entry::getValue)); + + for (var entry : appInstance.properties.entrySet()) { + if (requestProperties.has(entry.getKey())) { + continue; + } + requestProperties.add(entry.getKey(), entry.getValue()); + } + + // build UpdateAppInstance Request and pass the request to the + // handleUpdateAppInstanceRequest Method + var req = new UpdateAppInstance.Request(appInstance.instanceId, appInstance.alias, requestProperties); + this.handleUpdateAppInstanceRequest(user, req); + return new UpdateAppConfig.Response(); + } + + private UpdateAppConfig.Response updateComponentDirectly(User user, UpdateAppConfig.Request from) + throws OpenemsNamedException { + final var properties = from.properties(); + final var componentUpdateProps = new ArrayList(); + for (var key : properties.keySet()) { + componentUpdateProps.add(new UpdateComponentConfigRequest.Property(key, properties.get(key))); + } + final var updateRequest = new UpdateComponentConfigRequest(from.componentId(), componentUpdateProps); + this.componentManager.handleUpdateComponentConfigRequest(user, updateRequest); + return new UpdateAppConfig.Response(); } /** @@ -844,10 +1000,34 @@ public UpdateAppInstance.Response handleUpdateAppInstanceRequest(User user, Upda return this.lockModifyingApps(() -> { final var oldApp = this.findInstanceByIdOrError(request.instanceId()); final var app = this.findAppByIdOrError(oldApp.appId); - app.getAppConfiguration(ConfigurationTarget.UPDATE, request.properties(), user.getLanguage()); - final var updatedInstance = new OpenemsAppInstance(oldApp.appId, request.alias(), oldApp.instanceId, - request.properties(), oldApp.dependencies); + final var props = request.properties(); + final JsonObject restOfProps; + + if (app instanceof AbstractOpenemsAppWithProps) { + restOfProps = AbstractOpenemsApp.fillUpProperties(app, oldApp.properties); + } else { + restOfProps = oldApp.properties; + } + final var notAllowedProperties = props.keySet().stream()// + .filter(key -> { + if (restOfProps.has(key)) { + + return (!props.get(key).getAsString().equals(restOfProps.get(key).getAsString())); + } + return false; + }).filter(key -> { + final var canEdit = app.assertCanEdit(key, user); + return !canEdit; + }).collect(Collectors.joining(", ")); + if (notAllowedProperties.length() > 0) { + throw new OpenemsException("User is not allowed to edit " + notAllowedProperties + "!"); + } + + app.getAppConfiguration(ConfigurationTarget.UPDATE, props, user.getLanguage()); + + final var updatedInstance = new OpenemsAppInstance(oldApp.appId, request.alias(), oldApp.instanceId, props, + oldApp.dependencies); var result = this.lastUpdate = this.useAppManagerAppHelper(appHelper -> { return appHelper.updateApp(user, oldApp, updatedInstance, app); @@ -858,7 +1038,7 @@ public UpdateAppInstance.Response handleUpdateAppInstanceRequest(User user, Upda this.instantiatedApps.removeAll(result.modifiedOrCreatedApps); this.instantiatedApps.addAll(result.modifiedOrCreatedApps); - return new Pair<>(true, new UpdateAppInstance.Response( + return new Pair<>(!this.appIdIsAlwaysInstalled(app.getAppId()), new UpdateAppInstance.Response( this.createInstanceWithFilledProperties(app, result.rootInstance), result.warnings)); }, (shouldUpdate) -> { if (shouldUpdate == null || !shouldUpdate) { @@ -873,6 +1053,18 @@ public UpdateAppInstance.Response handleUpdateAppInstanceRequest(User user, Upda }); } + /** + * Checks from AppId if the app has the Flag alwaysInstalled. + * + * @param appId appId of the app + * @return if the app is always installed + */ + public boolean appIdIsAlwaysInstalled(String appId) { + return this.findAppById(appId)// + .map(t -> t.hasFlag(Flags.ALWAYS_INSTALLED))// + .orElse(false); + } + /** * updated the AppManager configuration with the given app instances. * @@ -883,7 +1075,7 @@ public UpdateAppInstance.Response handleUpdateAppInstanceRequest(User user, Upda private void updateAppManagerConfiguration(User user, List apps) throws OpenemsNamedException { this.waitingForModified = true; AppManagerImpl.sortApps(apps); - var p = new Property("apps", getJsonAppsString(apps)); + var p = new Property("apps", this.getJsonAppsString(apps)); // user can be null using internal method this.componentManager.handleUpdateComponentConfigRequest(user, new UpdateComponentConfigRequest(SINGLETON_COMPONENT_ID, Arrays.asList(p))); diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsApp.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsApp.java index b47cac80875..d31a2c54c60 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsApp.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsApp.java @@ -1,5 +1,7 @@ package io.openems.edge.core.appmanager; +import java.util.stream.Stream; + import org.osgi.service.component.ComponentConstants; import com.google.gson.JsonObject; @@ -14,6 +16,25 @@ public interface OpenemsApp { + /** + * Tests if a user is allowed to edit a property. + * + * @param prop The property to be tested + * @param user The user permissions are to be tested for + * @return true if user is allowed to edit, false otherwise + */ + public boolean assertCanEdit(String prop, User user); + + /** + * Maps the property name of a component to the coressponding app Property. + * + * @param prop The property to be mapped + * @param componentId the componentId + * @param instance instance of the app + * @return the mapped property name + */ + public String mapPropName(String prop, String componentId, OpenemsAppInstance instance); + /** * Gets the {@link AppAssistant} for this {@link OpenemsApp}. * @@ -123,6 +144,15 @@ public default Flag[] flags() { return new Flag[] {}; } + /** Checks whether the app has a passed flag set. + * + * @param flag the flag to be checked + * @return is the flag set + */ + public default boolean hasFlag(Flag flag) { + return Stream.of(this.flags()).anyMatch(f -> f.equals(flag)); + } + public static final String FALLBACK_IMAGE = """ data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY5\ 1AAABhWlDQ1BJQ0MgUHJvZmlsZQAAKM+VkT1Iw1AUhU9TpVIqgu0g4pChOlkQFXGUKBbBQmkrtOpg8tI/aNKQpLg4Cq4FB38Wqw\ diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsAppCategory.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsAppCategory.java index 16d616748e9..3bd86a01dde 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsAppCategory.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsAppCategory.java @@ -9,6 +9,11 @@ public enum OpenemsAppCategory { + /** + * Core. + */ + CORE("core"), + /** * Integrated Systems. */ diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelper.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelper.java index 2d5eb15d54b..8be75ab8167 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelper.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelper.java @@ -1,9 +1,13 @@ package io.openems.edge.core.appmanager.dependency; +import java.util.List; + import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.edge.common.user.User; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppInstance; +import io.openems.edge.core.appmanager.dependency.aggregatetask.AggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.AggregateTask.AggregateTaskExecutionConfiguration; public interface AppManagerAppHelper { @@ -42,6 +46,21 @@ public UpdateValues updateApp(User user, OpenemsAppInstance oldInstance, Openems */ public UpdateValues deleteApp(User user, OpenemsAppInstance instance) throws OpenemsNamedException; + /** + * Gets a list of {@link AggregateTaskExecutionConfiguration} which are the + * expected steps that are executed when installing a app with the provided + * properties. + * + * @param user the executing user + * @param instance the settings of the new {@link OpenemsAppInstance} + * @param app the {@link OpenemsApp} + * @return a list of the configurations + * @throws OpenemsNamedException on error + */ + public List getInstallConfiguration(// + User user, OpenemsAppInstance instance, OpenemsApp app // + ) throws OpenemsNamedException; + /** * Only available during a call of one of the other methods. * diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelperImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelperImpl.java index 7ce7533c0f2..3dd1c7d21c2 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelperImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelperImpl.java @@ -150,6 +150,47 @@ public UpdateValues deleteApp(User user, OpenemsAppInstance instance) throws Ope return this.usingTemporaryApps(user, () -> this.deleteAppInternal(user, instance)); } + @Override + public List getInstallConfiguration(// + User user, // + OpenemsAppInstance instance, // + OpenemsApp app // + ) throws OpenemsNamedException { + return this.getConfigurations(user, () -> this.updateAppInternal(user, null, instance, app)); + } + + private List getConfigurations(// + User user, // + ThrowingSupplier supplier // + ) throws OpenemsNamedException { + Objects.requireNonNull(supplier); + // to make sure the temporaryApps get set to null + this.resetTasks(); + this.temporaryApps = new TemporaryApps(); + OpenemsNamedException exception = null; + RuntimeException runtimeException = null; + try { + supplier.get(); + } catch (OpenemsNamedException e) { + exception = e; + } catch (RuntimeException e) { + runtimeException = e; + } + this.temporaryApps = null; + if (exception != null) { + this.log.error("An Exception occurred during handling the supplier.", exception); + throw exception; + } + if (runtimeException != null) { + this.log.error("An RuntimeException occurred during handling the supplier.", runtimeException); + throw runtimeException; + } + + return this.tasks.stream() // + .map(AggregateTask::getExecutionConfiguration) // + .toList(); + } + private UpdateValues usingTemporaryApps(User user, ThrowingSupplier supplier) throws OpenemsNamedException { Objects.requireNonNull(supplier); diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/AggregateTask.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/AggregateTask.java index b5a42dd4944..e32e34e0495 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/AggregateTask.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/AggregateTask.java @@ -5,6 +5,8 @@ import java.util.List; import java.util.Set; +import com.google.gson.JsonElement; + import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.session.Language; import io.openems.edge.common.user.User; @@ -12,6 +14,28 @@ public interface AggregateTask { + /** + * Class representing the configuration of an already aggregated + * {@link AggregateTask}. + */ + public interface AggregateTaskExecutionConfiguration { + + /** + * The identifier of the configuration. + * + * @return a string which identifies this type of configuration + */ + public String identifier(); + + /** + * Creates a {@link JsonElement} of this configuration. + * + * @return the created {@link JsonElement} + */ + public JsonElement toJson(); + + } + public static record AggregateTaskExecuteConstraints(// /** * Tasks which need to run before this task. @@ -48,6 +72,14 @@ public static record AggregateTaskExecuteConstraints(// */ public void delete(User user, List otherAppConfigurations) throws OpenemsNamedException; + /** + * Gets the {@link AggregateTaskExecutionConfiguration} which can be used for + * debugging. + * + * @return the AggregateTaskExecutionConfiguration + */ + public AggregateTaskExecutionConfiguration getExecutionConfiguration(); + /** * Validates the expected configuration. * diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImpl.java index 87fad0519b6..d7e878ddc73 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImpl.java @@ -4,6 +4,8 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; import java.util.stream.Collectors; import org.osgi.service.component.annotations.Activate; @@ -11,6 +13,9 @@ import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ServiceScope; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; + import io.openems.common.exceptions.InvalidValueException; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; @@ -20,12 +25,14 @@ import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest.Property; import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.user.User; import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.ComponentUtilImpl; import io.openems.edge.core.appmanager.TranslationUtil; import io.openems.edge.core.appmanager.dependency.AppManagerAppHelperImpl; +import io.openems.edge.core.appmanager.jsonrpc.GetEstimatedConfiguration; @Component(// service = { // @@ -37,6 +44,36 @@ ) public class ComponentAggregateTaskImpl implements ComponentAggregateTask { + private record ComponentAggregatedExecutionConfiguration(// + List components // + ) implements AggregateTask.AggregateTaskExecutionConfiguration { + + private ComponentAggregatedExecutionConfiguration { + Objects.requireNonNull(components); + } + + @Override + public String identifier() { + return "Component"; + } + + @Override + public JsonElement toJson() { + if (this.components.isEmpty()) { + return JsonNull.INSTANCE; + } + return JsonUtils.buildJsonObject() // + .add("components", this.components.stream() // + .map(t -> new GetEstimatedConfiguration.Component(t.getFactoryId(), t.getId(), t.getAlias(), + t.getProperties().entrySet().stream() // + .collect(JsonUtils.toJsonObject(Entry::getKey, Entry::getValue)))) // + .map(GetEstimatedConfiguration.Component.serializer()::serialize) // + .collect(JsonUtils.toJsonArray())) // + .build(); + } + + } + private final ComponentManager componentManager; private List components; @@ -210,6 +247,11 @@ public void delete(User user, List otherAppConfigurations) thr } } + @Override + public AggregateTaskExecutionConfiguration getExecutionConfiguration() { + return new ComponentAggregatedExecutionConfiguration(this.components); + } + @Override public String getGeneralFailMessage(Language l) { final var bundle = AppManagerAppHelperImpl.getTranslationBundle(l); diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/PersistencePredictorAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/PersistencePredictorAggregateTaskImpl.java index 49bfb12f7ed..7ce66695a85 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/PersistencePredictorAggregateTaskImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/PersistencePredictorAggregateTaskImpl.java @@ -6,6 +6,7 @@ import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Stream; @@ -15,12 +16,15 @@ import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ServiceScope; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; import com.google.gson.JsonPrimitive; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest; import io.openems.common.session.Language; +import io.openems.common.utils.JsonUtils; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.user.User; @@ -38,6 +42,42 @@ ) public class PersistencePredictorAggregateTaskImpl implements PersistencePredictorAggregateTask { + private record PersistencePredictorExecutionConfiguration(// + Set channelsToAdd, // + Set channelsToRemove // + ) implements AggregateTask.AggregateTaskExecutionConfiguration { + + private PersistencePredictorExecutionConfiguration { + Objects.requireNonNull(channelsToAdd); + Objects.requireNonNull(channelsToRemove); + } + + @Override + public String identifier() { + return "PersistencePredictor"; + } + + @Override + public JsonElement toJson() { + if (this.channelsToAdd.isEmpty() && this.channelsToRemove.isEmpty()) { + return JsonNull.INSTANCE; + } + return JsonUtils.buildJsonObject() // + .onlyIf(!this.channelsToAdd.isEmpty(), t -> { + t.add("channelsToAdd", this.channelsToAdd.stream() // + .map(JsonPrimitive::new) // + .collect(JsonUtils.toJsonArray())); + }) // + .onlyIf(!this.channelsToRemove.isEmpty(), t -> { + t.add("channelsToRemove", this.channelsToRemove.stream() // + .map(JsonPrimitive::new) // + .collect(JsonUtils.toJsonArray())); + }) // + .build(); + } + + } + private ComponentManager componentManager; private Set channelsToAdd; @@ -97,6 +137,11 @@ public void validate(// errors.add("Missing channels in predictor [" + String.join(";", missingChannels) + "]"); } + @Override + public AggregateTaskExecutionConfiguration getExecutionConfiguration() { + return new PersistencePredictorExecutionConfiguration(this.channelsToAdd, this.channelsToRemove); + } + @Override public String getGeneralFailMessage(Language l) { final var bundle = AppManagerAppHelperImpl.getTranslationBundle(l); diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTaskImpl.java index 2059bc85e89..41d903bbb14 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTaskImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTaskImpl.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.Set; import org.osgi.service.component.annotations.Activate; @@ -10,8 +11,13 @@ import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ServiceScope; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonPrimitive; + import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.session.Language; +import io.openems.common.utils.JsonUtils; import io.openems.edge.common.user.User; import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.ComponentUtil; @@ -28,6 +34,33 @@ ) public class SchedulerAggregateTaskImpl implements SchedulerAggregateTask { + private record SchedulerExecutionConfiguration(// + List insertOrder // + ) implements AggregateTask.AggregateTaskExecutionConfiguration { + + private SchedulerExecutionConfiguration { + Objects.requireNonNull(insertOrder); + } + + @Override + public String identifier() { + return "Scheduler"; + } + + @Override + public JsonElement toJson() { + if (this.insertOrder.isEmpty()) { + return JsonNull.INSTANCE; + } + return JsonUtils.buildJsonObject() // + .add("insertOrder", this.insertOrder.stream() // + .map(JsonPrimitive::new) // + .collect(JsonUtils.toJsonArray())) + .build(); + } + + } + private final ComponentAggregateTask aggregateTask; private final ComponentUtil componentUtil; @@ -92,6 +125,11 @@ public void delete(User user, List otherAppConfigurations) thr this.componentUtil.removeIdsInSchedulerIfExisting(user, this.removeIds); } + @Override + public AggregateTaskExecutionConfiguration getExecutionConfiguration() { + return new SchedulerExecutionConfiguration(this.order); + } + @Override public String getGeneralFailMessage(Language l) { final var bundle = AppManagerAppHelperImpl.getTranslationBundle(l); diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerByCentralOrderAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerByCentralOrderAggregateTaskImpl.java index 253f2b98c45..6d608d70b4d 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerByCentralOrderAggregateTaskImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerByCentralOrderAggregateTaskImpl.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.function.Function; @@ -20,9 +21,13 @@ import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ServiceScope; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; + import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; import io.openems.common.session.Language; +import io.openems.common.utils.JsonUtils; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.user.User; import io.openems.edge.core.appmanager.AppConfiguration; @@ -44,6 +49,37 @@ ) public class SchedulerByCentralOrderAggregateTaskImpl implements SchedulerByCentralOrderAggregateTask { + private record SchedulerByCentralOrderExecutionConfiguration(// + List insertOrder // + ) implements AggregateTask.AggregateTaskExecutionConfiguration { + + private SchedulerByCentralOrderExecutionConfiguration { + Objects.requireNonNull(insertOrder); + } + + @Override + public String identifier() { + return "SchedulerByCentralOrder"; + } + + @Override + public JsonElement toJson() { + if (this.insertOrder.isEmpty()) { + return JsonNull.INSTANCE; + } + return JsonUtils.buildJsonObject() // + .add("insertOrder", this.insertOrder.stream() // + .map(t -> JsonUtils.buildJsonObject() // + .addProperty("id", t.id()) // + .addProperty("factoryId", t.factoryId()) // + .addPropertyIfNotNull("createdByAppId", t.createdByAppId()) // + .build()) + .collect(JsonUtils.toJsonArray())) + .build(); + } + + } + private final ComponentManager componentManager; private final ComponentUtil componentUtil; private final AppManagerUtil appManagerUtil; @@ -68,6 +104,7 @@ public ProductionSchedulerOrderDefinition() { .filterByFactoryId("Controller.Api.ModbusTcp.ReadWrite") // .thenByCreatedAppId("App.Ess.GeneratingPlantController") // .rest()) // + .thenByFactoryId("Controller.Api.ModbusRtu.ReadWrite") // .thenByFactoryId("Controller.Api.Rest.ReadWrite") // .thenByFactoryId("Controller.Ess.GridOptimizedCharge") // .thenByFactoryId("Controller.Ess.Hybrid.Surplus-Feed-To-Grid") // @@ -416,6 +453,11 @@ public void delete(User user, List otherAppConfigurations) thr this.componentUtil.removeIdsInSchedulerIfExisting(user, this.getIdsToRemove(otherAppConfigurations)); } + @Override + public AggregateTaskExecutionConfiguration getExecutionConfiguration() { + return new SchedulerByCentralOrderExecutionConfiguration(this.schedulerComponents); + } + private List getIdsToRemove(List otherAppConfigurations) { final var otherIds = AppConfiguration .flatMap(otherAppConfigurations, SchedulerByCentralOrderAggregateTask.class, diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTaskImpl.java index b01d6dde1a3..bbe8a3c9896 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTaskImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTaskImpl.java @@ -2,6 +2,7 @@ import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import org.osgi.service.component.annotations.Activate; @@ -9,8 +10,12 @@ import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ServiceScope; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; + import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.session.Language; +import io.openems.common.utils.JsonUtils; import io.openems.edge.common.user.User; import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.ComponentUtil; @@ -28,6 +33,40 @@ ) public class StaticIpAggregateTaskImpl implements StaticIpAggregateTask { + private record StaticIpExecutionConfiguration(// + List ips // + ) implements AggregateTask.AggregateTaskExecutionConfiguration { + + private StaticIpExecutionConfiguration { + Objects.requireNonNull(ips); + } + + @Override + public String identifier() { + return "StaticIp"; + } + + @Override + public JsonElement toJson() { + if (this.ips.isEmpty()) { + return JsonNull.INSTANCE; + } + return JsonUtils.buildJsonObject() // + .add("interfaces", this.ips.stream() // + .map(t -> JsonUtils.buildJsonObject() // + .addProperty("interface", t.interfaceName) // + .add("addresses", t.getIps().stream() // + .map(ip -> JsonUtils.buildJsonObject() // + .addProperty("address", ip.getInet4Address().getHostAddress()) // + .build()) // + .collect(JsonUtils.toJsonArray())) // + .build()) + .collect(JsonUtils.toJsonArray())) + .build(); + } + + } + private final boolean isWindows = System.getProperty("os.name").startsWith("Windows"); private final ComponentUtil componentUtil; @@ -69,6 +108,11 @@ public void delete(User user, List otherAppConfigurations) thr this.execute(user, otherAppConfigurations, null, this.ips2Delete); } + @Override + public AggregateTaskExecutionConfiguration getExecutionConfiguration() { + return new StaticIpExecutionConfiguration(this.ips); + } + private void execute(// final User user, // final List otherAppConfigurations, // diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/flag/Flags.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/flag/Flags.java index bb85f9bbcf2..57c4e53a00e 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/flag/Flags.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/flag/Flags.java @@ -3,6 +3,8 @@ public final class Flags { public static final Flag SHOW_AFTER_KEY_REDEEM = new FlagRecord("showAfterKeyRedeem"); + + public static final Flag ALWAYS_INSTALLED = new FlagRecord("alwaysInstalled"); private Flags() { super(); diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetEstimatedConfiguration.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetEstimatedConfiguration.java new file mode 100644 index 00000000000..f2b10cf9853 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetEstimatedConfiguration.java @@ -0,0 +1,125 @@ +package io.openems.edge.core.appmanager.jsonrpc; + +import static io.openems.common.jsonrpc.serialization.JsonSerializerUtil.jsonObjectSerializer; +import static io.openems.common.utils.JsonUtils.toJsonArray; +import static java.util.Collections.emptyList; + +import java.util.List; +import java.util.Objects; + +import com.google.gson.JsonObject; + +import io.openems.common.jsonrpc.serialization.JsonSerializer; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.common.jsonapi.EndpointRequestType; +import io.openems.edge.core.appmanager.dependency.aggregatetask.AggregateTask; +import io.openems.edge.core.appmanager.jsonrpc.GetEstimatedConfiguration.Request; +import io.openems.edge.core.appmanager.jsonrpc.GetEstimatedConfiguration.Response; + +public class GetEstimatedConfiguration implements EndpointRequestType { + + @Override + public String getMethod() { + return "getEstimatedConfiguration"; + } + + @Override + public JsonSerializer getRequestSerializer() { + return Request.serializer(); + } + + @Override + public JsonSerializer getResponseSerializer() { + return Response.serializer(); + } + + public static record Request(// + String appId, // + String alias, // + JsonObject properties // + ) { + + /** + * Returns a {@link JsonSerializer} for a + * {@link GetEstimatedConfiguration.Request}. + * + * @return the created {@link JsonSerializer} + */ + public static JsonSerializer serializer() { + return jsonObjectSerializer(Request.class, // + json -> new Request(// + json.getString("appId"), // + json.getString("alias"), // + json.getJsonObject("properties")), + obj -> JsonUtils.buildJsonObject() // + .addProperty("appId", obj.appId()) // + .addProperty("alias", obj.alias()) // + .add("properties", obj.properties()) // + .build()); + } + + } + + public record Response(// + List configurations // + ) { + + /** + * Returns a {@link JsonSerializer} for a + * {@link GetEstimatedConfiguration.Response}. + * + * @return the created {@link JsonSerializer} + */ + public static JsonSerializer serializer() { + return jsonObjectSerializer(Response.class, // + // TODO polymorphic serializer + json -> new Response(emptyList()), // + obj -> JsonUtils.buildJsonObject() // + .add("configurations", obj.configurations().stream() // + .map(t -> { + final var configJson = t.toJson(); + + if (configJson.isJsonNull()) { + return null; + } + + return JsonUtils.buildJsonObject() // + .addProperty("type", t.identifier()) // + .add("configuration", configJson) // + .build(); + }) // + .filter(Objects::nonNull) // + .collect(toJsonArray())) // + .build()); + } + } + + public record Component(String factoryId, String id, String alias, JsonObject properties) { + + /** + * Returns a {@link JsonSerializer} for a + * {@link GetEstimatedConfiguration.Component}. + * + * @return the created {@link JsonSerializer} + */ + public static JsonSerializer serializer() { + return jsonObjectSerializer(GetEstimatedConfiguration.Component.class, json -> { + return new GetEstimatedConfiguration.Component(// + json.getString("factoryId"), // + json.getString("id"), // + json.getString("alias"), // + json.getJsonObject("properties") // + ); + }, obj -> { + return JsonUtils.buildJsonObject() // + .addProperty("factoryId", obj.factoryId()) // + .addProperty("id", obj.id()) // + .addProperty("alias", obj.alias()) // + .add("properties", obj.properties()) // + .build(); + }); + } + + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/UpdateAppConfig.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/UpdateAppConfig.java new file mode 100644 index 00000000000..0253c0742a3 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/UpdateAppConfig.java @@ -0,0 +1,102 @@ +package io.openems.edge.core.appmanager.jsonrpc; + +import static io.openems.common.jsonrpc.serialization.JsonSerializerUtil.jsonObjectSerializer; + +import com.google.gson.JsonObject; + +import io.openems.common.jsonrpc.serialization.JsonSerializer; +import io.openems.common.jsonrpc.serialization.JsonSerializerUtil; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.common.jsonapi.EndpointRequestType; +import io.openems.edge.core.appmanager.OpenemsAppInstance; +import io.openems.edge.core.appmanager.jsonrpc.UpdateAppConfig.Request; +import io.openems.edge.core.appmanager.jsonrpc.UpdateAppConfig.Response; + +/** + * Updates an {@link OpenemsAppInstance}. + * + *

+ * Request: + * + *

+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": "UUID",
+ *   "method": "updateAppConfig",
+ *   "params": {
+ *     "componentId": string (uuid),
+ *     "properties": {}
+ *   }
+ * }
+ * 
+ * + *

+ * Response: + * + *

+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": "UUID",
+ *   "result": {
+ *     "instance": {@link OpenemsAppInstance#toJsonObject()}
+ *     "warnings": string[]
+ *   }
+ * }
+ * 
+ */ +public class UpdateAppConfig implements EndpointRequestType { + + @Override + public String getMethod() { + return "updateAppConfig"; + } + + @Override + public JsonSerializer getRequestSerializer() { + return Request.serializer(); + } + + @Override + public JsonSerializer getResponseSerializer() { + return Response.serializer(); + } + + public record Request(// + String componentId, // + JsonObject properties // + ) { + + /** + * Returns a {@link JsonSerializer} for a {@link UpdateAppInstance.Request}. + * + * @return the created {@link JsonSerializer} + */ + public static JsonSerializer serializer() { + return jsonObjectSerializer(UpdateAppConfig.Request.class, // + json -> new UpdateAppConfig.Request(// + json.getString("componentId"), // + json.getJsonObject("properties")), // + obj -> JsonUtils.buildJsonObject() // + .addProperty("componentId", obj.componentId()) // + .add("properties", obj.properties()) // + .build()); + } + + } + + public record Response(// + + ) { + + /** + * Returns a {@link JsonSerializer} for a {@link UpdateAppInstance.Response}. + * + * @return the created {@link JsonSerializer} + */ + public static JsonSerializer serializer() { + return JsonSerializerUtil.emptyObjectSerializer(Response::new); + } + + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties index e9795f28fb0..620bdbb678a 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties @@ -15,6 +15,7 @@ ess = Speichersystemsteuerung openemsDeviceHardware = OpenEMS Geräte Hardware timedata = Timedata test = Test +core = Core # Global alias = Alias @@ -77,15 +78,30 @@ formly.validation.requireChecked = Diese Checkbox ist erforderlich! App.Api.apiTimeout.label = Api-Timeout App.Api.apiTimeout.description = Legt die Zeitüberschreitung in Sekunden für Aktualisierungen in den von dieser Api eingestellten Kanälen fest. -App.Api.ModbusTcp.changeComponentHint = Hinweis: Beim Einfügen einer Komponente ändert sich auch die Modbus Tabelle! +App.Api.Modbus.componentIds.description = Komponenten, die über die Schnittstelle verfügbar gemacht werden sollen. +App.Api.Modbus.componentIds.label = Komponenten-IDs +App.Api.Modbus.changeComponentHint = Hinweis: Beim Einfügen einer Komponente ändert sich auch die Modbus Tabelle! App.Api.ModbusTcp.ReadOnly.Name = Modbus/TCP lesend App.Api.ModbusTcp.ReadOnly.Name.short = Modbus/TCP lesend - App.Api.ModbusTcp.ReadWrite.Name = Modbus/TCP Schreibzugriff App.Api.ModbusTcp.ReadWrite.Name.short = Modbus/TCP Schreibzugriff -App.Api.ModbusTcp.ReadWrite.componentIds.label = Component-IDs -App.Api.ModbusTcp.ReadWrite.componentIds.description = Komponenten, die über die Schnittstelle verfügbar gemacht werden sollen. + +App.Api.ModbusRtu.ReadOnly.Name = Modbus/RTU lesend +App.Api.ModbusRtu.ReadOnly.Name.short = Modbus/RTU lesend +App.Api.ModbusRtu.ReadWrite.Name = Modbus/RTU Schreibzugriff +App.Api.ModbusRtu.ReadWrite.Name.short = Modbus/RTU Schreibzugriff + +App.Api.ModbusRtu.portName.label = Port-Name +App.Api.ModbusRtu.portName.description = Der Name des seriellen Ports - z.B. '/dev/ttyUSB0' oder 'COM3' +App.Api.ModbusRtu.baudrate.label = Baudrate +App.Api.ModbusRtu.baudrate.description = Die Baudrate - z.B. 9600, 19200, 38400, 57600 oder 115200 +App.Api.ModbusRtu.databits.label = Datenbits +App.Api.ModbusRtu.databits.description = Die Anzahl der Datenbits - z.B. 8 +App.Api.ModbusRtu.stopbits.label = Stoppbits +App.Api.ModbusRtu.stopbits.description = Die Anzahl der Stoppbits - '1', '1.5' oder '2' +App.Api.ModbusRtu.parity.label = Parität +App.Api.ModbusRtu.parity.description = Die Parität - 'keine', 'gerade', 'ungerade', 'mark' oder 'space' App.Api.Mqtt.Name = MQTT-Api lesend App.Api.Mqtt.Name.short = MQTT-Api lesend @@ -120,6 +136,13 @@ App.Timedata.InfluxDb.isReadOnly.description = Aktiviert Read Only Modus. Dann w App.Timedata.InfluxDb.bucket.label = Bucket App.Timedata.InfluxDb.bucket.description = Der Bucket-Name; für InfluxDB v1: 'Datenbank/retentionPolicy', z. B. 'db/daten' +# Core +App.Core.Meta.Name = Meta +App.Core.Meta.Name.short = Meta + +App.Core.Meta.currency.label = Währung +App.Core.Meta.gridCharge.label = Ist Beladung aus dem Netz erlaubt? +App.Core.Meta.gridCharge.description = ACHTUNG: Die aktive Beladung der Batterie aus dem Netz über die technisch notwendige Erhaltungsladung hinaus erfordert eine entsprechende Anmeldung des Speichersystems. Im Zweifel wenden Sie sich hierzu bitte an Ihren Installateur. Mit Betätigung des blauen \\\"Speichern\\\"-Buttons bestätigen Sie, dass Sie diese Prüfung durchgeführt haben.

Beachten Sie: Voraussetzung für die Inanspruchnahme einer Förderung nach dem Gesetz für den Ausbau erneuerbarer Energien (EEG) ist, dass im Speicher ausschließlich Strom zwischengespeichert wird, der aus erneuerbaren Energien und/oder Grubengas stammt. Bei Aktivierung dieser Funktion wird der Speicher mit Netzstrom geladen. Dieser Netzstrom wird, je nach Stromlieferungsvertrag, ggf. auch aus fossilen Energieträgern und/oder Atomkraft gewonnen. Daher können wir nicht gewährleisten, dass der Speicher ausschließlich mit Strom aus erneuerbaren Energien und/oder Grubengas geladen wird # Evcs App.Evcs.controller.alias = Ladestation Steuerung @@ -253,7 +276,7 @@ App.IntegratedSystem.ctRatioFirst.label = Wandler-Primärstrom (200A - 5000A/5A) App.IntegratedSystem.shadowManagementDisabled.label = Schattenmanagement deaktivieren App.IntegratedSystem.shadowManagementDisabled.description = Nur wenn Optimierer verbaut sind, muss das Schattenmanagement deaktiviert werden App.IntegratedSystem.hasEssLimiter14a.label = Hat Limitierer für §14a -App.IntegratedSystem.naProtectionEnabled.label = NA-Schutz aktiviert? +App.IntegratedSystem.naProtectionEnabled.label = Fernabschaltung aktivieren (Zentraler NA Schutz) App.IntegratedSystem.modbusToBattery.alias = Kommunikation mit der Batterie App.IntegratedSystem.modbusToBatteryN.alias = Kommunikation mit den Batterien diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties index 03f34b1bafa..5441df63c3a 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties @@ -15,6 +15,7 @@ ess = Energy Storage controller openemsDeviceHardware = OpenEMS Device Hardware timedata = Timedata test = Test +core = Core # Global alias = Alias @@ -77,15 +78,30 @@ formly.validation.requireChecked = This checkbox is required! App.Api.apiTimeout.label = Api-Timeout App.Api.apiTimeout.description = Sets the timeout in seconds for updates on Channels set by this Api. +App.Api.Modbus.componentIds.description = Components that should be made available via Modbus. +App.Api.Modbus.componentIds.label = Component-IDs App.Api.ModbusTcp.changeComponentHint = Note: When inserting a component, the Modbus table also changes! +App.Api.ModbusRtu.ReadOnly.Name = Modbus/RTU reading +App.Api.ModbusRtu.ReadOnly.Name.short = Modbus/RTU reading +App.Api.ModbusRtu.ReadWrite.Name = Modbus/RTU write access +App.Api.ModbusRtu.ReadWrite.Name.short = Modbus/RTU write access + App.Api.ModbusTcp.ReadOnly.Name = Modbus/TCP reading App.Api.ModbusTcp.ReadOnly.Name.short = Modbus/TCP reading - App.Api.ModbusTcp.ReadWrite.Name = Modbus/TCP write access App.Api.ModbusTcp.ReadWrite.Name.short = Modbus/TCP write access -App.Api.ModbusTcp.ReadWrite.componentIds.label = Component-IDs -App.Api.ModbusTcp.ReadWrite.componentIds.description = Components that should be made available via Modbus. + +App.Api.ModbusRtu.portName.label = Port-Name +App.Api.ModbusRtu.portName.description = The name of the serial port - e.g. '/dev/ttyUSB0' or 'COM3' +App.Api.ModbusRtu.baudrate.label = Baudrate +App.Api.ModbusRtu.baudrate.description = The baudrate - e.g. 9600, 19200, 38400, 57600 or 115200 +App.Api.ModbusRtu.databits.label = Databits +App.Api.ModbusRtu.databits.description = The number of databits - e.g. 8 +App.Api.ModbusRtu.stopbits.label = Stopbits +App.Api.ModbusRtu.stopbits.description = The number of stopbits - '1', '1.5' or '2'. +App.Api.ModbusRtu.parity.label = Parity +App.Api.ModbusRtu.parity.description = The parity - 'none', 'even', 'odd', 'mark' or 'space' App.Api.Mqtt.Name = MQTT-Api reading App.Api.Mqtt.Name.short = MQTT-Api reading @@ -120,6 +136,13 @@ App.Timedata.InfluxDb.isReadOnly.description = Activates the read-only mode. The App.Timedata.InfluxDb.bucket.label = Bucket App.Timedata.InfluxDb.bucket.description = The bucket name; for InfluxDB v1: 'database/retentionPolicy', e.g. 'db/data' +# Core +App.Core.Meta.Name = Meta +App.Core.Meta.Name.short = Meta + +App.Core.Meta.currency.label = Currency +App.Core.Meta.gridCharge.label = Is charging from the grid allowed? +App.Core.Meta.gridCharge.description =
WARNING: Actively charging the battery from the grid beyond the technically necessary maintenance charge requires appropriate registration of the storage system. If in doubt, please consult your installer. By pressing the blue \\\"Save\\\" button, you confirm that you have conducted this check.

Please note: A requirement for receiving subsidies under the Renewable Energy Sources Act (EEG) is that only electricity generated from renewable energies and/or mine gas is stored in the system. When this function is activated, the storage system is charged with grid electricity. This grid electricity may, depending on your electricity supply contract, include energy from fossil fuels and/or nuclear power. Therefore, we cannot guarantee that the storage system will only be charged with electricity from renewable energies and/or mine gas. # Evcs App.Evcs.controller.alias = Charging station control @@ -252,8 +275,8 @@ App.IntegratedSystem.gridMeterType.option.commercialMeter = Home 3-phase sensor App.IntegratedSystem.ctRatioFirst.label = CT-Ratio (200A - 5000A/5A) App.IntegratedSystem.shadowManagementDisabled.label = Deactivate shadow management App.IntegratedSystem.shadowManagementDisabled.description = Only if optimisers are installed, shadow management must be deactivated -App.IntegratedSystem.hasEssLimiter14a.label = Has limiter for §14a -App.IntegratedSystem.naProtectionEnabled.label = NA-protection enabled? +App.IntegratedSystem.hasEssLimiter14a.label = Has limiter for §14a +App.IntegratedSystem.naProtectionEnabled.label = Activate remote shutdown (central NA protection) App.IntegratedSystem.modbusToBattery.alias = Communication with the battery App.IntegratedSystem.modbusToBatteryN.alias = Communication with the batteries diff --git a/io.openems.edge.core/src/io/openems/edge/core/componentmanager/ComponentManagerImpl.java b/io.openems.edge.core/src/io/openems/edge/core/componentmanager/ComponentManagerImpl.java index 3029e0d6a04..90494546d22 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/componentmanager/ComponentManagerImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/componentmanager/ComponentManagerImpl.java @@ -1,5 +1,6 @@ package io.openems.edge.core.componentmanager; +import static java.util.Collections.emptyMap; import static java.util.stream.Collectors.toMap; import java.io.IOException; @@ -10,9 +11,12 @@ import java.util.Arrays; import java.util.Collections; import java.util.Dictionary; +import java.util.HashMap; import java.util.Hashtable; import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.osgi.framework.BundleContext; @@ -54,6 +58,7 @@ import io.openems.common.types.ChannelAddress; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.JsonUtils; +import io.openems.common.utils.StreamUtils; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.channel.EnumDoc; import io.openems.edge.common.channel.StateChannelDoc; @@ -156,6 +161,20 @@ protected void deactivate() { } } + @Override + public Map getComponentProperties(String componentId) { + Configuration config; + try { + config = this.getExistingConfigForId(componentId); + } catch (OpenemsNamedException e) { + e.printStackTrace(); + return emptyMap(); + } + + return StreamUtils.dictionaryToStream(config.getProperties()) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + } + @Override public List getEnabledComponents() { return this.getComponentsViaService("(&(enabled=true)(!(service.factoryPid=Core.ComponentManager)))"); diff --git a/io.openems.edge.core/test/io/openems/edge/app/TestPermissions.java b/io.openems.edge.core/test/io/openems/edge/app/TestPermissions.java new file mode 100644 index 00000000000..4f560f26e8f --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/app/TestPermissions.java @@ -0,0 +1,169 @@ +package io.openems.edge.app; + +import static io.openems.edge.app.common.props.CommonProps.defaultDef; + +import java.util.ArrayList; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.function.Function; + +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.Reference; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.common.utils.JsonUtils.JsonArrayBuilder; +import io.openems.edge.app.TestPermissions.Property; +import io.openems.edge.app.TestPermissions.TestPermissionsParameter; +import io.openems.edge.app.common.props.CommonProps; +import io.openems.edge.app.common.props.ComponentProps; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter.BundleProvider; +import io.openems.edge.core.appmanager.dependency.Tasks; +import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; +import io.openems.edge.core.appmanager.formly.builder.FormlyBuilder; +import io.openems.edge.core.appmanager.formly.builder.ReorderArrayBuilder; +import io.openems.edge.core.appmanager.formly.builder.ReorderArrayBuilder.SelectOption; +import io.openems.edge.core.appmanager.formly.enums.DisplayType; + +/** + * Tests AppPropertyPermissions. + */ +@org.osgi.service.component.annotations.Component(name = "App.Test.TestPermissions") +public class TestPermissions extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public record TestPermissionsParameter(// + ResourceBundle bundle // + ) implements BundleProvider { + + } + + public static enum Property implements Type { + ID(AppDef.componentId("id0")), ADMIN_ONLY(AppDef.copyOfGeneric(CommonProps.defaultDef(), def -> def // + .setMinRole(Role.ADMIN))), // + INSTALLER_ONLY(AppDef.copyOfGeneric(CommonProps.defaultDef(), def -> def // + .setMinRole(Role.INSTALLER))), // + EVERYONE(AppDef.copyOfGeneric(CommonProps.defaultDef())), // + UPDATE_ARRAY(AppDef.copyOfGeneric(defaultDef(), def -> def // + .setTranslatedLabel("component.id.plural") // + .setField(JsonFormlyUtil::buildFieldGroupFromNameable, (app, property, l, parameter, field) -> { + field.setPopupInput(property, DisplayType.STRING); + + final var arrayBuilder = new ReorderArrayBuilder(property); // + final var fields = JsonUtils.buildJsonArray() // + .add(arrayBuilder.build()); + + field.setFieldGroup(fields.build()); + })).setDefaultValue((app, property, l, parameter) -> { + return JsonUtils.buildJsonArray().add("val1").add("val2").build(); + })), + + ;// + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, TestPermissionsParameter> getParamter() { + return t -> { + return new TestPermissionsParameter(// + createResourceBundle(t.language) // + ); + }; + } + + } + + @Activate + public TestPermissions(// + @Reference ComponentManager componentManager, // + ComponentContext componentContext, // + @Reference ConfigurationAdmin cm, // + @Reference ComponentUtil componentUtil // + ) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + + final var components = new ArrayList(); + final var updateArray = this.getJsonArray(p, Property.UPDATE_ARRAY); + components.add(new EdgeConfig.Component(this.getId(t, p, Property.ADMIN_ONLY, "id0"), "alias", "factoryId", // + new JsonObject())); + components.add( + new EdgeConfig.Component(this.getId(t, p, Property.INSTALLER_ONLY, "id0"), "alias", "factoryId", // + new JsonObject())); + components.add(new EdgeConfig.Component(this.getId(t, p, Property.EVERYONE, "id0"), "alias", "factoryId", // + new JsonObject())); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); + }; + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.TEST }; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE; + } + + @Override + protected TestPermissions getApp() { + return this; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + +} diff --git a/io.openems.edge.core/test/io/openems/edge/app/api/TestModbusTcpApiReadWrite.java b/io.openems.edge.core/test/io/openems/edge/app/api/TestModbusTcpApiReadWrite.java index 874f365b240..f0bba295098 100644 --- a/io.openems.edge.core/test/io/openems/edge/app/api/TestModbusTcpApiReadWrite.java +++ b/io.openems.edge.core/test/io/openems/edge/app/api/TestModbusTcpApiReadWrite.java @@ -44,8 +44,8 @@ public void testDeactivateReadOnly() throws Exception { var readOnlyApp = this.appManagerTestBundle.sut.getInstantiatedApps().get(0); if (readOnlyApp.properties.has("ACTIVE")) { - var isActiv = readOnlyApp.properties.get("ACTIVE").getAsBoolean(); - assertTrue(isActiv); + var isActive = readOnlyApp.properties.get("ACTIVE").getAsBoolean(); + assertTrue(isActive); } // create ReadWrite app @@ -66,8 +66,8 @@ public void testDeactivateReadOnly() throws Exception { readOnlyApp = this.appManagerTestBundle.sut.getInstantiatedApps().get(0); assertTrue(readOnlyApp.properties.has("ACTIVE")); - var isActiv = readOnlyApp.properties.get("ACTIVE").getAsBoolean(); - assertFalse(isActiv); + var isActive = readOnlyApp.properties.get("ACTIVE").getAsBoolean(); + assertFalse(isActive); // remove ReadWrite to see if the ReadOnly gets activated this.appManagerTestBundle.sut.handleDeleteAppInstanceRequest(DUMMY_ADMIN, @@ -77,8 +77,8 @@ public void testDeactivateReadOnly() throws Exception { readOnlyApp = this.appManagerTestBundle.sut.getInstantiatedApps().get(0); if (readOnlyApp.properties.has("ACTIVE")) { - isActiv = readOnlyApp.properties.get("ACTIVE").getAsBoolean(); - assertTrue(isActiv); + isActive = readOnlyApp.properties.get("ACTIVE").getAsBoolean(); + assertTrue(isActive); } } diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppPropertyPermissionsTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppPropertyPermissionsTest.java new file mode 100644 index 00000000000..601c29cf203 --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppPropertyPermissionsTest.java @@ -0,0 +1,82 @@ +package io.openems.edge.core.appmanager; + +import static io.openems.edge.common.test.DummyUser.DUMMY_ADMIN; +import static io.openems.edge.common.test.DummyUser.DUMMY_INSTALLER; +import static io.openems.edge.common.test.DummyUser.DUMMY_OWNER; + +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.TestPermissions; +import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; +import io.openems.edge.core.appmanager.jsonrpc.UpdateAppConfig; + +public class AppPropertyPermissionsTest { + + private AppManagerTestBundle appManagerTestBundle; + + private TestPermissions testPermissions; + + @Before + public void setUp() throws Exception { + this.appManagerTestBundle = new AppManagerTestBundle(null, null, t -> { + return ImmutableList.of(// + this.testPermissions = Apps.testPermissions(t) // + ); + }); + this.appManagerTestBundle.sut.handleAddAppInstanceRequest(DUMMY_ADMIN, + new AddAppInstance.Request(this.testPermissions.getAppId(), "key", "alias", JsonUtils.buildJsonObject() // + .addProperty(TestPermissions.Property.ID.name(), "id0") + .addProperty(TestPermissions.Property.ADMIN_ONLY.name(), "val0") // + .addProperty(TestPermissions.Property.INSTALLER_ONLY.name(), "val0") // + .addProperty(TestPermissions.Property.EVERYONE.name(), "val0") // + .build())) + .instance(); + } + + @Test(expected = OpenemsException.class) + public void testAdminOnlyAsInstaller() throws OpenemsNamedException { + final var req = this.request("ADMIN_ONLY"); + this.appManagerTestBundle.sut.handleUpdateAppConfigRequest(DUMMY_INSTALLER, req); + } + + @Test + public void testAdminOnlyAsAdmin() throws OpenemsNamedException { + final var req = this.request("ADMIN_ONLY"); + this.appManagerTestBundle.sut.handleUpdateAppConfigRequest(DUMMY_ADMIN, req); + } + + @Test(expected = OpenemsException.class) + public void testInstallerOnlyAsOwner() throws OpenemsNamedException { + final var req = this.request("INSTALLER_ONLY"); + this.appManagerTestBundle.sut.handleUpdateAppConfigRequest(DUMMY_OWNER, req); + } + + @Test + public void testInstallerOnlyAsAdmin() throws OpenemsNamedException { + final var req = this.request("INSTALLER_ONLY"); + this.appManagerTestBundle.sut.handleUpdateAppConfigRequest(DUMMY_ADMIN, req); + } + + @Test + public void testEveryoneAsOwner() throws OpenemsNamedException { + final var req = this.request("EVERYONE"); + this.appManagerTestBundle.sut.handleUpdateAppConfigRequest(DUMMY_OWNER, req); + } + + private UpdateAppConfig.Request request(String val) { + final var ja = JsonUtils.buildJsonArray().add("val3").add("val4").build(); + final var jo = JsonUtils.buildJsonObject().add(val, JsonUtils.getAsJsonElement("val1")) + .add("UPDATE_ARRAY", ja) + .build(); + + final var req = new UpdateAppConfig.Request("id0", jo); + return req; + } + +} diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java index 3d776848849..6bf3134da74 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java @@ -12,6 +12,9 @@ import io.openems.edge.app.TestBDependencyToC; import io.openems.edge.app.TestC; import io.openems.edge.app.TestMultipleIds; +import io.openems.edge.app.TestPermissions; +import io.openems.edge.app.api.ModbusRtuApiReadOnly; +import io.openems.edge.app.api.ModbusRtuApiReadWrite; import io.openems.edge.app.api.ModbusTcpApiReadOnly; import io.openems.edge.app.api.ModbusTcpApiReadWrite; import io.openems.edge.app.api.RestJsonApiReadOnly; @@ -321,6 +324,16 @@ public static final TechbaseCm4sGen2 techbaseCm4sGen2(AppManagerTestBundle t) { return app(t, TechbaseCm4sGen2::new, "App.OpenemsHardware.CM4S.Gen2"); } + /** + * Test method for creating a {@link TestPermissions}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final TestPermissions testPermissions(AppManagerTestBundle t) { + return app(t, TestPermissions::new, "App.Test.TestPermissions"); + } + // Test /** @@ -385,6 +398,26 @@ public static final ModbusTcpApiReadWrite modbusTcpApiReadWrite(AppManagerTestBu return app(t, ModbusTcpApiReadWrite::new, "App.Api.ModbusTcp.ReadWrite"); } + /** + * Test method for creating a {@link ModbusRtuApiReadOnly}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final ModbusRtuApiReadOnly modbusRtuApiReadOnly(AppManagerTestBundle t) { + return app(t, ModbusRtuApiReadOnly::new, "App.Api.ModbusRtu.ReadOnly"); + } + + /** + * Test method for creating a {@link ModbusRtuApiReadWrite}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final ModbusRtuApiReadWrite modbusRtuApiReadWrite(AppManagerTestBundle t) { + return app(t, ModbusRtuApiReadWrite::new, "App.Api.ModbusRtu.ReadWrite"); + } + /** * Test method for creating a {@link RestJsonApiReadOnly}. * diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyApp.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyApp.java index 245f4d5d20a..4965436cfa5 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyApp.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyApp.java @@ -8,6 +8,7 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.function.TriFunction; import io.openems.common.oem.OpenemsEdgeOem; import io.openems.common.session.Language; import io.openems.edge.common.user.User; @@ -26,6 +27,7 @@ public class DummyApp implements OpenemsApp { private final AppAssistant appAssistant; private final AppDescriptor appDescriptor; private final ThrowingTriFunction configuration; + private final TriFunction propName; public static class DummyAppBuilder { @@ -42,6 +44,7 @@ public static class DummyAppBuilder { private AppAssistant appAssistant; private AppDescriptor appDescriptor; private ThrowingTriFunction configuration; + private TriFunction propName; private DummyAppBuilder() { } @@ -115,6 +118,11 @@ public DummyAppBuilder setConfiguration( return this; } + public DummyAppBuilder setPropName(TriFunction propName) { + this.propName = propName; + return this; + } + public DummyApp build() { final var name = this.name == null ? this.appId : this.name; return new DummyApp(// @@ -128,8 +136,8 @@ public DummyApp build() { this.properties.toArray(OpenemsAppPropertyDefinition[]::new), // this.appAssistant == null ? AppAssistant.create(name).build() : this.appAssistant, // this.appDescriptor == null ? AppDescriptor.create().build() : this.appDescriptor, // - this.configuration == null ? (t, u, s) -> AppConfiguration.empty() : this.configuration // - ); + this.configuration == null ? (t, u, s) -> AppConfiguration.empty() : this.configuration, // + this.propName == null ? (t, u, s) -> t : this.propName); } } @@ -155,8 +163,8 @@ public DummyApp(// AppAssistant appAssistant, // AppDescriptor appDescriptor, // ThrowingTriFunction configuration // - ) { + Language, AppConfiguration, OpenemsNamedException> configuration, // + TriFunction mapPropName) { super(); this.appId = appId; this.categories = categories; @@ -169,6 +177,7 @@ public DummyApp(// this.appAssistant = appAssistant; this.appDescriptor = appDescriptor; this.configuration = configuration; + this.propName = mapPropName; } @Override @@ -232,4 +241,13 @@ public OpenemsAppPermissions getAppPermissions() { return this.appPermissions; } + @Override + public String mapPropName(String prop, String componentId, OpenemsAppInstance instance) { + return this.propName.apply(prop, componentId, instance); + } + + @Override + public boolean assertCanEdit(String prop, User user) { + return true; + } } diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java index 9ad1f56bc01..91ab3fa14c2 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java @@ -14,6 +14,7 @@ import io.openems.edge.core.appmanager.dependency.TemporaryApps; import io.openems.edge.core.appmanager.dependency.UpdateValues; import io.openems.edge.core.appmanager.dependency.aggregatetask.AggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.AggregateTask.AggregateTaskExecutionConfiguration; public class DummyAppManagerAppHelper implements AppManagerAppHelper { @@ -63,6 +64,12 @@ public UpdateValues deleteApp(User user, OpenemsAppInstance instance) throws Ope return this.impl.deleteApp(user, instance); } + @Override + public List getInstallConfiguration(User user, OpenemsAppInstance instance, + OpenemsApp app) throws OpenemsNamedException { + return this.impl.getInstallConfiguration(user, instance, app); + } + @Override public TemporaryApps getTemporaryApps() { return this.impl.getTemporaryApps(); diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyPseudoComponentManager.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyPseudoComponentManager.java index 9aba2d766eb..e2c8af440f4 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyPseudoComponentManager.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyPseudoComponentManager.java @@ -36,6 +36,7 @@ import io.openems.common.types.EdgeConfig.ActualEdgeConfig; import io.openems.common.types.EdgeConfig.Component; import io.openems.common.utils.JsonUtils; +import io.openems.common.utils.StreamUtils; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.component.OpenemsComponent; @@ -389,4 +390,15 @@ public ServiceReference getServiceReference() { } + @Override + public Map getComponentProperties(String componentId) { + try { + var dic = this.getComponent(componentId).getComponentContext().getProperties(); + return StreamUtils.dictionaryToStream(dic) // + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + } catch (OpenemsNamedException e) { + return Collections.emptyMap(); + } + } + } diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/UpdateComponentDirectlyTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/UpdateComponentDirectlyTest.java new file mode 100644 index 00000000000..dc0f9136cd0 --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/UpdateComponentDirectlyTest.java @@ -0,0 +1,48 @@ +package io.openems.edge.core.appmanager; + +import static io.openems.edge.common.test.DummyUser.DUMMY_ADMIN; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.request.CreateComponentConfigRequest; +import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.core.appmanager.AppManagerTestBundle.PseudoComponentManagerFactory; +import io.openems.edge.core.appmanager.jsonrpc.UpdateAppConfig; + +public class UpdateComponentDirectlyTest { + private AppManagerTestBundle appManagerTestBundle; + + @Before + public void setUp() throws Exception { + this.appManagerTestBundle = new AppManagerTestBundle(null, null, t -> { + return ImmutableList.of(// + + ); + }, null, new PseudoComponentManagerFactory()); + } + + @Test + public void testAdminOnlyAsInstaller() throws OpenemsNamedException { + final var testTest = List.of(new UpdateComponentConfigRequest.Property("id", "evcs1"), + new UpdateComponentConfigRequest.Property("debugMode", false)); + this.appManagerTestBundle.componentManger.handleCreateComponentConfigRequest(DUMMY_ADMIN, + new CreateComponentConfigRequest("Evcs.Keba.KeContact", testTest)); + this.appManagerTestBundle.sut.handleUpdateAppConfigRequest(DUMMY_ADMIN, + new UpdateAppConfig.Request("evcs1", JsonUtils.buildJsonObject().addProperty("debugMode", true)// + .build())); + final var properties = JsonUtils.buildJsonObject()// + .addProperty("debugMode", true)// + .build(); + this.appManagerTestBundle + .assertComponentExist(new EdgeConfig.Component("evcs1", "", "Evcs.Keba.KeContact", properties)); + + } +} diff --git a/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/ess/Edge2EdgeEss.java b/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/ess/Edge2EdgeEss.java index f80c88ee79a..bc21f46680e 100644 --- a/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/ess/Edge2EdgeEss.java +++ b/io.openems.edge.edge2edge/src/io/openems/edge/edge2edge/ess/Edge2EdgeEss.java @@ -12,11 +12,13 @@ public interface Edge2EdgeEss extends OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { MINIMUM_POWER_SET_POINT(Doc.of(OpenemsType.FLOAT) // - .accessMode(AccessMode.READ_ONLY)// - .unit(Unit.WATT)), // + .accessMode(AccessMode.READ_ONLY) // + .unit(Unit.WATT) // + .text("Minimum available active power")), // MAXIMUM_POWER_SET_POINT(Doc.of(OpenemsType.FLOAT) // .accessMode(AccessMode.READ_ONLY)// - .unit(Unit.WATT)), // + .unit(Unit.WATT) // + .text("Maximum available electrical power")), // REMOTE_SET_ACTIVE_POWER_EQUALS(Doc.of(OpenemsType.FLOAT) // .accessMode(AccessMode.WRITE_ONLY)// .unit(Unit.WATT)), // diff --git a/io.openems.edge.ess.api/src/io/openems/edge/ess/api/ManagedSymmetricEss.java b/io.openems.edge.ess.api/src/io/openems/edge/ess/api/ManagedSymmetricEss.java index 36565ec140f..d840fe149d3 100644 --- a/io.openems.edge.ess.api/src/io/openems/edge/ess/api/ManagedSymmetricEss.java +++ b/io.openems.edge.ess.api/src/io/openems/edge/ess/api/ManagedSymmetricEss.java @@ -68,6 +68,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { SET_ACTIVE_POWER_EQUALS(new IntegerDoc() // .unit(Unit.WATT) // .accessMode(AccessMode.WRITE_ONLY) // + .text("Write command for a charge power (-) or discharge power (+). Range e.g. [-5000 to 5000]") // .onChannelSetNextWrite( new PowerConstraint("SetActivePowerEquals", Phase.ALL, Pwr.ACTIVE, Relationship.EQUALS))), @@ -120,6 +121,7 @@ public void accept(ManagedSymmetricEss ess, Integer value) throws OpenemsNamedEx SET_REACTIVE_POWER_EQUALS(new IntegerDoc() // .unit(Unit.VOLT_AMPERE_REACTIVE) // .accessMode(AccessMode.WRITE_ONLY) // + .text("Write command for the reactive power") // .onChannelSetNextWrite( new PowerConstraint("SetReactivePowerEquals", Phase.ALL, Pwr.REACTIVE, Relationship.EQUALS))), // /** @@ -135,6 +137,7 @@ public void accept(ManagedSymmetricEss ess, Integer value) throws OpenemsNamedEx SET_ACTIVE_POWER_LESS_OR_EQUALS(new IntegerDoc() // .unit(Unit.WATT) // .accessMode(AccessMode.WRITE_ONLY) // + .text("Write command for a minimum charge power (-) or maximum discharge power (+). Range e.g. [-5000 to 5000]") // .onChannelSetNextWrite(new PowerConstraint("SetActivePowerLessOrEquals", Phase.ALL, Pwr.ACTIVE, Relationship.LESS_OR_EQUALS))), // /** @@ -150,6 +153,7 @@ public void accept(ManagedSymmetricEss ess, Integer value) throws OpenemsNamedEx SET_ACTIVE_POWER_GREATER_OR_EQUALS(new IntegerDoc() // .unit(Unit.WATT) // .accessMode(AccessMode.WRITE_ONLY) // + .text("Write command for a maximum charge power (-) or minimum discharge power (+). Range e.g. [-5000 to 5000]") // .onChannelSetNextWrite(new PowerConstraint("SetActivePowerGreaterOrEquals", Phase.ALL, Pwr.ACTIVE, Relationship.GREATER_OR_EQUALS))), // /** @@ -165,6 +169,7 @@ public void accept(ManagedSymmetricEss ess, Integer value) throws OpenemsNamedEx SET_REACTIVE_POWER_LESS_OR_EQUALS(new IntegerDoc() // .unit(Unit.VOLT_AMPERE_REACTIVE) // .accessMode(AccessMode.WRITE_ONLY) // + .text("Write command for the maximum reactive power") // .onChannelSetNextWrite(new PowerConstraint("SetReactivePowerLessOrEquals", Phase.ALL, Pwr.REACTIVE, Relationship.LESS_OR_EQUALS))), // /** @@ -180,6 +185,7 @@ public void accept(ManagedSymmetricEss ess, Integer value) throws OpenemsNamedEx SET_REACTIVE_POWER_GREATER_OR_EQUALS(new IntegerDoc() // .unit(Unit.VOLT_AMPERE_REACTIVE) // .accessMode(AccessMode.WRITE_ONLY) // + .text("Write command for the maximum reactive power") // .onChannelSetNextWrite(new PowerConstraint("SetReactivePowerGreaterOrEquals", Phase.ALL, Pwr.REACTIVE, Relationship.GREATER_OR_EQUALS))), // /** diff --git a/io.openems.edge.ess.api/src/io/openems/edge/ess/api/SymmetricEss.java b/io.openems.edge.ess.api/src/io/openems/edge/ess/api/SymmetricEss.java index 4f14108fd5e..04e27c3b2e1 100644 --- a/io.openems.edge.ess.api/src/io/openems/edge/ess/api/SymmetricEss.java +++ b/io.openems.edge.ess.api/src/io/openems/edge/ess/api/SymmetricEss.java @@ -32,7 +32,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ SOC(Doc.of(OpenemsType.INTEGER) // .unit(Unit.PERCENT) // - .persistencePriority(PersistencePriority.HIGH)), + .persistencePriority(PersistencePriority.HIGH) // + .text("State of Charge of the energy storage system")), // /** * Capacity. * @@ -57,7 +58,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { * */ GRID_MODE(Doc.of(GridMode.values()) // - .persistencePriority(PersistencePriority.HIGH)), + .persistencePriority(PersistencePriority.HIGH) // + .text("Current power grid mode; 1:On-Grid, 2:Off-Grid")), // /** * Active Power. * @@ -71,7 +73,9 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { ACTIVE_POWER(Doc.of(OpenemsType.INTEGER) // .unit(Unit.WATT) // .persistencePriority(PersistencePriority.HIGH) // - .text("Negative values for Charge; positive for Discharge") // + .text("Discharge or charging Power (including DC-PV power, if applicable)." + + " For the actual charging or discharging power of the battery, please refer to address" + + " \"ess0/DcDischargePower\". Negative values for charge; positive for discharge.") // ), /** * Reactive Power. @@ -85,6 +89,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { REACTIVE_POWER(Doc.of(OpenemsType.INTEGER) // .unit(Unit.VOLT_AMPERE_REACTIVE) // .persistencePriority(PersistencePriority.HIGH) // + .text("Current value of the reactive power")// ), /** * Holds the currently maximum possible apparent power. This value is commonly @@ -138,7 +143,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ MIN_CELL_VOLTAGE(Doc.of(OpenemsType.INTEGER) // .unit(Unit.MILLIVOLT) // - .persistencePriority(PersistencePriority.HIGH)), + .persistencePriority(PersistencePriority.HIGH) // + .text("Minimum cell voltage")), // /** * Max Cell Voltage. * @@ -153,7 +159,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ MAX_CELL_VOLTAGE(Doc.of(OpenemsType.INTEGER) // .unit(Unit.MILLIVOLT) // - .persistencePriority(PersistencePriority.HIGH)), + .persistencePriority(PersistencePriority.HIGH) // + .text("Maximum cell voltage")), // /** * Min Cell Temperature. * @@ -168,7 +175,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ MIN_CELL_TEMPERATURE(Doc.of(OpenemsType.INTEGER) // .unit(Unit.DEGREE_CELSIUS) // - .persistencePriority(PersistencePriority.HIGH)), + .persistencePriority(PersistencePriority.HIGH) // + .text("Minimum cell temperature")), // /** * Max Cell Temperature. * @@ -183,7 +191,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ MAX_CELL_TEMPERATURE(Doc.of(OpenemsType.INTEGER) // .unit(Unit.DEGREE_CELSIUS) // - .persistencePriority(PersistencePriority.HIGH)); + .persistencePriority(PersistencePriority.HIGH) // + .text("Maximum cell temperature")); // private final Doc doc; diff --git a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadUtils.java b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadUtils.java index 3f0c194c5a1..1777480da1c 100644 --- a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadUtils.java +++ b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadUtils.java @@ -27,6 +27,7 @@ public class HardyBarthReadUtils { private final EvcsHardyBarthImpl parent; private int errorCounter = 0; + private int undefinedErrorCounter = 0; public HardyBarthReadUtils(EvcsHardyBarthImpl parent) { this.parent = parent; @@ -58,9 +59,20 @@ protected void setEvcsChannelIds(JsonElement json, PhaseRotation phaseRotation) "secc", "port0", "metering", "energy", "active_import", "actual")); // Current - final var currentL1 = getAsIntOrElse(json, 0, "secc", "port0", "metering", "current", "ac", "l1", "actual"); - final var currentL2 = getAsIntOrElse(json, 0, "secc", "port0", "metering", "current", "ac", "l2", "actual"); - final var currentL3 = getAsIntOrElse(json, 0, "secc", "port0", "metering", "current", "ac", "l3", "actual"); + final var currentL1 = getAsInteger(json, 1, "secc", "port0", "metering", "current", "ac", "l1", "actual"); + final var currentL2 = getAsInteger(json, 1, "secc", "port0", "metering", "current", "ac", "l2", "actual"); + final var currentL3 = getAsInteger(json, 1, "secc", "port0", "metering", "current", "ac", "l3", "actual"); + + // Checks if value are null and if they are checks if its the third error or + // more + // and otherwise returns, so that no new values are written this cycle + // TODO: find a better long term solution + if (currentL1 == null || currentL2 == null || currentL3 == null) { + this.parent.logDebug("Invalid current values detected"); + if (this.handleUndefinedError()) { + return; + } + } // Power final var activePowerL1 = getAsInteger(json, SCALE_FACTOR_MINUS_1, // @@ -75,6 +87,20 @@ protected void setEvcsChannelIds(JsonElement json, PhaseRotation phaseRotation) final var voltageL2 = activePowerL2 == null ? null : round(activePowerL2 * 1_000_000F / currentL2); final var voltageL3 = activePowerL3 == null ? null : round(activePowerL3 * 1_000_000F / currentL3); + if (activePowerL1 == null || activePowerL2 == null || activePowerL3 == null) { + this.parent.logDebug("Active power values are null"); + if (this.handleUndefinedError()) { + return; + } + } + + if (voltageL1 == null || voltageL2 == null || voltageL3 == null) { + this.parent.logDebug("Voltage values are null"); + if (this.handleUndefinedError()) { + return; + } + } + var rp = RotatedPhases.from(phaseRotation, // voltageL1, currentL1, activePowerL1, // voltageL2, currentL2, activePowerL2, // @@ -101,7 +127,16 @@ protected void setEvcsChannelIds(JsonElement json, PhaseRotation phaseRotation) "secc", "port0", "metering", "power", "active_total", "actual")) // .map(p -> p < 100 ? 0 : p) // Ignore the consumption of the charger itself .orElse(null); + + if (activePower == null) { + this.parent.logDebug("Active Power invalid"); + if (this.handleUndefinedError()) { + return; + } + } + this.parent._setActivePower(activePower); + this.undefinedErrorCounter = 0; // STATUS var status = getValueFromJson(STRING, json, value -> { @@ -163,13 +198,9 @@ private static Integer getAsInteger(JsonElement json, float scaleFactor, String. jsonPaths); } - private static int getAsIntOrElse(JsonElement json, int orElse, String... jsonPaths) { - var result = getValueFromJson(INTEGER, json, // - value -> TypeUtils.getAsType(INTEGER, value), // - jsonPaths); - return result == null // - ? orElse // - : result; + private boolean handleUndefinedError() { + this.undefinedErrorCounter++; + return this.undefinedErrorCounter <= 3; } /** diff --git a/io.openems.edge.evcs.hardybarth/test/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImplTest.java b/io.openems.edge.evcs.hardybarth/test/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImplTest.java index 8ecb6c334bc..1d352a5a9b5 100644 --- a/io.openems.edge.evcs.hardybarth/test/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImplTest.java +++ b/io.openems.edge.evcs.hardybarth/test/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImplTest.java @@ -107,6 +107,55 @@ public void test() throws Exception { ); } + @Test + public void testHandleUndefinedCheck() throws Exception { + final var phaseRotation = L2_L3_L1; + var sut = new EvcsHardyBarthImpl(); + var ru = sut.readUtils; + new ComponentTest(sut) // + .addReference("httpBridgeFactory", ofDummyBridge()) // + .activate(MyConfig.create() // + .setId("evcs0") // + .setIp("192.168.8.101") // + .setMaxHwCurrent(32_000) // + .setMinHwCurrent(6_000) // + .setPhaseRotation(phaseRotation).build()) + + .next(new TestCase() // + .onBeforeProcessImage(() -> ru + .handleGetApiCallResponse(new HttpResponse(OK, API_RESPONSE), phaseRotation)) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 3192) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, 1044) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, 1075) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, 1073) // + .output(ElectricityMeter.ChannelId.CURRENT, 14_770) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, 4_770) // + .output(ElectricityMeter.ChannelId.CURRENT_L2, 5_000) // + .output(ElectricityMeter.ChannelId.CURRENT_L3, 5_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE, 216_156) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 218_868) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L2, 215_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L3, 214_600) // + ) + // Values are not overwritten when empty/null response from api + .next(new TestCase() // + .onBeforeProcessImage(() -> ru.handleGetApiCallResponse( + new HttpResponse(OK, EMPTY_API_RESPONSE), phaseRotation)) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 3192) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, 1044) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, 1075) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, 1073) // + .output(ElectricityMeter.ChannelId.CURRENT, 14_770) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, 4_770) // + .output(ElectricityMeter.ChannelId.CURRENT_L2, 5_000) // + .output(ElectricityMeter.ChannelId.CURRENT_L3, 5_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE, 216_156) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 218_868) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L2, 215_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L3, 214_600) // + ); + } + private static final String API_RESPONSE = """ { "device":{ @@ -275,4 +324,174 @@ public void test() throws Exception { } } """; + + private static final String EMPTY_API_RESPONSE = """ + { + "device":{ + "product":null, + "modelname":null, + "hardware_version":null, + "software_version":null, + "vcs_version":null, + "hostname":null, + "mac_address":null, + "serial":null, + "uuid":null, + "internal_id":null + }, + "secc":{ + "port0":{ + "ci":{ + "evse":{ + "basic":{ + "grid_current_limit":{ + "actual":null + }, + "phase_count":null, + "physical_current_limit":null, + "offered_current_limit":null + }, + "phase":{ + "actual":null + } + }, + "charge":{ + "cp":{ + "status":null + }, + "plug":{ + "status":null + }, + "contactor":{ + "status":null + }, + "pwm":{ + "status":null + } + } + }, + "salia":{ + "chargemode":null, + "thermal":null, + "mem":null, + "uptime":null, + "load":null, + "chargedata":null, + "authmode":null, + "firmwarestate":null, + "firmwareprogress":null, + "heartbeat":null, + "pausecharging":null + }, + "session":{ + "authorization_status":null + }, + "contactor":{ + "state":{ + "hlc_target":null, + "actual":null, + "target":null + }, + "error":null + }, + "metering":{ + "meter":{ + "serialnumber":null, + "type":null, + "available":null + }, + "eichrecht_protocol":null, + "power":{ + "active":{ + "ac":{ + "l1":{ + "actual":null + }, + "l2":{ + "actual":null + }, + "l3":{ + "actual":null + } + } + }, + "active_total":{ + "actual":null + } + }, + "current":{ + "ac":{ + "l1":{ + "actual":null + }, + "l2":{ + "actual":null + }, + "l3":{ + "actual":null + } + } + }, + "energy":{ + "active_total":{ + "actual":null + }, + "active_export":{ + "actual":null + }, + "active_import":{ + "actual":null + } + } + }, + "emergency_shutdown":null, + "rcd":{ + "feedback":{ + "available":null + }, + "state":{ + "actual":null + }, + "recloser":{ + "available":null + } + }, + "plug_lock":{ + "state":{ + "actual":null, + "target":null + }, + "error":null + }, + "availability":{ + "actual":null + }, + "cp":{ + "pwm_state":{ + "actual":null + }, + "state":null, + "duty_cycle":null + }, + "rfid":{ + "available":null, + "authorizereq":null + }, + "diode_present":null, + "cable_current_limit":null, + "ready_for_slac":null, + "ev_present":null, + "ventilation":{ + "state":{ + "actual":null + }, + "available":null + }, + "charging":null, + "grid_current_limit":null + } + } + } + """; + } diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContact.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContact.java index 2de80ca9f6b..4e2487d2df0 100644 --- a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContact.java +++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContact.java @@ -15,7 +15,6 @@ import io.openems.edge.common.modbusslave.ModbusType; import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.ManagedEvcs; -import io.openems.edge.evcs.api.Status; import io.openems.edge.meter.api.ElectricityMeter; public interface EvcsKebaKeContact @@ -46,13 +45,12 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { /* * Report 2 */ - STATUS_KEBA(Doc.of(Status.values()) // - .text("Current state of the charging station")), + R2_STATE(Doc.of(R2State.values())), // ERROR_1(Doc.of(OpenemsType.INTEGER) // .text("Detail code for state ERROR; exceptions see FAQ on www.kecontact.com")), // ERROR_2(Doc.of(OpenemsType.INTEGER) // .text("Detail code for state ERROR; exceptions see FAQ on www.kecontact.com")), // - PLUG(Doc.of(Plug.values())), // + R2_PLUG(Doc.of(R2Plug.values())), // ENABLE_SYS(Doc.of(OpenemsType.BOOLEAN) // .text("Enable state for charging (contains Enable input, RFID, UDP,..)")), // ENABLE_USER(Doc.of(OpenemsType.BOOLEAN) // @@ -145,10 +143,10 @@ private ModbusSlaveNatureTable getModbusSlaveNatureTable(AccessMode accessMode) .channel(16, EvcsKebaKeContact.ChannelId.SERIAL, ModbusType.STRING16) .channel(32, EvcsKebaKeContact.ChannelId.FIRMWARE, ModbusType.STRING16) .channel(48, EvcsKebaKeContact.ChannelId.COM_MODULE, ModbusType.STRING16) - .channel(64, EvcsKebaKeContact.ChannelId.STATUS_KEBA, ModbusType.UINT16) + .channel(64, EvcsKebaKeContact.ChannelId.R2_STATE, ModbusType.UINT16) .channel(65, EvcsKebaKeContact.ChannelId.ERROR_1, ModbusType.UINT16) .channel(66, EvcsKebaKeContact.ChannelId.ERROR_2, ModbusType.UINT16) - .channel(67, EvcsKebaKeContact.ChannelId.PLUG, ModbusType.UINT16) + .channel(67, EvcsKebaKeContact.ChannelId.R2_PLUG, ModbusType.UINT16) .channel(68, EvcsKebaKeContact.ChannelId.ENABLE_SYS, ModbusType.UINT16) .channel(69, EvcsKebaKeContact.ChannelId.ENABLE_USER, ModbusType.UINT16) .channel(70, EvcsKebaKeContact.ChannelId.MAX_CURR_PERCENT, ModbusType.UINT16) diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/Plug.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/R2Plug.java similarity index 53% rename from io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/Plug.java rename to io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/R2Plug.java index 1ca34e1c7a6..148a3540b86 100644 --- a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/Plug.java +++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/R2Plug.java @@ -2,18 +2,41 @@ import io.openems.common.types.OptionsEnum; -public enum Plug implements OptionsEnum { +public enum R2Plug implements OptionsEnum { UNDEFINED(-1, "Undefined"), // + /** + * No cable is plugged. + */ UNPLUGGED(0, "Unplugged"), // + /** + * Cable is plugged into charging station. + */ PLUGGED_ON_EVCS(1, "Plugged on EVCS"), // + /** + * Cable is plugged into charging station and locked. + * + *

+ * This is the default idle state for all devices with permanently attached + * cable. + */ PLUGGED_ON_EVCS_AND_LOCKED(3, "Plugged on EVCS and locked"), // + /** + * Cable is plugged into charging station and vehicle but not locked. + */ PLUGGED_ON_EVCS_AND_ON_EV(5, "Plugged on EVCS and on EV"), // + /** + * Cable is plugged into charging station and vehicle, furthermore the cable is + * locked. + * + *

+ * Charging is not possible until plug state "7" is reached. + */ PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED(7, "Plugged on EVCS and on EV and locked"); private final int value; private final String name; - private Plug(int value, String name) { + private R2Plug(int value, String name) { this.value = value; this.name = name; } diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/R2State.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/R2State.java new file mode 100644 index 00000000000..ac27b229300 --- /dev/null +++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/R2State.java @@ -0,0 +1,56 @@ +package io.openems.edge.evcs.keba.kecontact; + +import io.openems.common.types.OptionsEnum; + +public enum R2State implements OptionsEnum { + UNDEFINED(-1, "Undefined"), // + /** + * Startup. + */ + STARTUP(0, "Startup"), // + /** + * Not ready for charging. Charging station is not connected to a vehicle, is + * locked by the authorization function or another mechanism. + */ + NOT_READY(1, "Not ready"), // + /** + * Ready for charging and waiting for reaction from vehicle. + */ + READY(2, "Ready"), // + /** + * Charging. + */ + CHARGING(3, "Charging"), // + /** + * Error is present. + */ + ERROR(4, "Error"), // + /** + * Charging process temporarily interrupted because temperature is too high or + * any other voter denies. + */ + INTERRUPTED(5, "Interrupted"); + + private final int value; + private final String name; + + private R2State(int value, String name) { + this.value = value; + this.name = name; + } + + @Override + public int getValue() { + return this.value; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public OptionsEnum getUndefined() { + return UNDEFINED; + } +} \ No newline at end of file diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadHandler.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadHandler.java index 69208c18c90..a19fca836c3 100644 --- a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadHandler.java +++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadHandler.java @@ -1,5 +1,6 @@ package io.openems.edge.evcs.keba.kecontact; +import static io.openems.common.utils.FunctionUtils.doNothing; import static io.openems.common.utils.JsonUtils.getAsOptionalInt; import static io.openems.common.utils.JsonUtils.getAsOptionalLong; import static io.openems.common.utils.JsonUtils.getAsOptionalString; @@ -31,6 +32,7 @@ public class ReadHandler implements Consumer { private final Logger log = LoggerFactory.getLogger(ReadHandler.class); private final EvcsKebaKeContactImpl parent; + private final EnergySessionHandler energySessionHandler = new EnergySessionHandler(); private boolean receiveReport1 = false; private boolean receiveReport2 = false; @@ -98,45 +100,45 @@ public void accept(String message) { */ case "2" -> { this.receiveReport2 = true; - this.setInt(EvcsKebaKeContact.ChannelId.STATUS_KEBA, j, "State"); + + // Parse Status and Plug immediately + this.setInt(EvcsKebaKeContact.ChannelId.R2_STATE, j, "State"); + final R2State r2State = keba.channel(EvcsKebaKeContact.ChannelId.R2_STATE).getNextValue().asEnum(); + final R2Plug r2Plug = this.setPlug(j); // Value "setenergy" not used, because it is reset by the currtime 0 1 command // Set Evcs status - Channel stateChannel = keba.channel(EvcsKebaKeContact.ChannelId.STATUS_KEBA); - Channel plugChannel = keba.channel(EvcsKebaKeContact.ChannelId.PLUG); - - Plug plug = plugChannel.value().asEnum(); - Status status = stateChannel.value().asEnum(); - if (plug.equals(Plug.PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED)) { - - // Charging is rejected (by the Software) if the plug is connected but the EVCS - // still not ready for charging. - if (status.equals(Status.NOT_READY_FOR_CHARGING)) { - status = Status.CHARGING_REJECTED; - } + var status = switch (r2Plug) { + case PLUGGED_ON_EVCS, PLUGGED_ON_EVCS_AND_LOCKED, PLUGGED_ON_EVCS_AND_ON_EV, UNDEFINED, UNPLUGGED // + -> Status.NOT_READY_FOR_CHARGING; + case PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED -> { /* * Check if the maximum energy limit is reached, informs the user and sets the * status */ - int limit = keba.getSetEnergyLimit().orElse(0); - int energy = keba.getEnergySession().orElse(0); - if (energy >= limit && limit != 0) { - status = Status.ENERGY_LIMIT_REACHED; + var limit = keba.getSetEnergyLimit().get(); + var energy = keba.getEnergySession().get(); + if (limit != null && energy != null && energy >= limit) { + yield Status.ENERGY_LIMIT_REACHED; } - } else { - // Plug not fully connected - status = Status.NOT_READY_FOR_CHARGING; + yield switch (r2State) { + case UNDEFINED -> Status.UNDEFINED; + case STARTUP -> Status.STARTING; + case NOT_READY -> Status.NOT_READY_FOR_CHARGING; + case INTERRUPTED, READY -> Status.READY_FOR_CHARGING; + case CHARGING -> Status.CHARGING; + case ERROR -> Status.ERROR; + }; } + }; keba._setStatus(status); - var errorState = status == Status.ERROR == true; - keba.channel(EvcsKebaKeContact.ChannelId.CHARGINGSTATION_STATE_ERROR).setNextValue(errorState); + keba.channel(EvcsKebaKeContact.ChannelId.CHARGINGSTATION_STATE_ERROR).setNextValue(status == Status.ERROR); this.setInt(EvcsKebaKeContact.ChannelId.ERROR_1, j, "Error1"); this.setInt(EvcsKebaKeContact.ChannelId.ERROR_2, j, "Error2"); - this.setInt(EvcsKebaKeContact.ChannelId.PLUG, j, "Plug"); this.setBoolean(EvcsKebaKeContact.ChannelId.ENABLE_SYS, j, "Enable sys"); this.setBoolean(EvcsKebaKeContact.ChannelId.ENABLE_USER, j, "Enable user"); this.setInt(EvcsKebaKeContact.ChannelId.MAX_CURR_PERCENT, j, "Max curr %"); @@ -198,9 +200,10 @@ public void accept(String message) { .map(e -> round(e * 0.1F)) // .orElse(null)); keba._setEnergySession(// - getAsOptionalInt(j, "E pres") // - .map(e -> round(e * 0.1F)) // - .orElse(null)); + this.energySessionHandler.updateFromReport3(// + getAsOptionalInt(j, "E pres") // + .map(e -> round(e * 0.1F)) // + .orElse(null))); // TODO use COS_PHI to calculate ReactivePower this.setInt(EvcsKebaKeContact.ChannelId.COS_PHI, j, "PF"); @@ -240,10 +243,10 @@ public void accept(String message) { */ default -> { if (j.has("State")) { - this.setInt(EvcsKebaKeContact.ChannelId.STATUS_KEBA, j, "State"); + this.setInt(EvcsKebaKeContact.ChannelId.R2_STATE, j, "State"); } if (j.has("Plug")) { - this.setInt(EvcsKebaKeContact.ChannelId.PLUG, j, "Plug"); + this.setPlug(j); } if (j.has("Input")) { this.setBoolean(EvcsKebaKeContact.ChannelId.INPUT, j, "Input"); @@ -384,6 +387,14 @@ private void setString(ChannelId channelId, JsonObject jMessage, String name) { this.set(channelId, getAsOptionalString(jMessage, name).orElse(null)); } + private R2Plug setPlug(JsonObject jMessage) { + final var channelId = EvcsKebaKeContact.ChannelId.R2_PLUG; + this.setInt(channelId, jMessage, "Plug"); + final R2Plug r2Plug = this.parent.channel(channelId).getNextValue().asEnum(); + this.energySessionHandler.updatePlug(r2Plug); + return r2Plug; + } + private void setInt(ChannelId channelId, JsonObject jMessage, String name) { this.set(channelId, getAsOptionalInt(jMessage, name).orElse(null)); } @@ -423,4 +434,36 @@ public boolean hasResultandReset(Report report) { } return result; } + + protected static class EnergySessionHandler { + private R2Plug r2Plug = R2Plug.UNDEFINED; + private Integer ePresOnUnplugged = null; + + protected synchronized void updatePlug(R2Plug r2Plug) { + this.r2Plug = r2Plug; + } + + public synchronized Integer updateFromReport3(Integer ePres) { + switch (this.r2Plug) { + case UNPLUGGED, // no cable + PLUGGED_ON_EVCS, PLUGGED_ON_EVCS_AND_LOCKED // not plugged on EV + -> this.ePresOnUnplugged = ePres > 0 ? ePres : null; + case UNDEFINED, // unsure + PLUGGED_ON_EVCS_AND_ON_EV, PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED // plugged on EV + -> doNothing(); + } + if (this.ePresOnUnplugged == null) { + return ePres; + } + if (ePres == null) { + return null; + } + if (this.ePresOnUnplugged <= ePres) { + return null; + } + // reset + this.ePresOnUnplugged = null; + return ePres; + } + } } diff --git a/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContactImplTest.java b/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContactImplTest.java index db60737ebb8..5ed2f067016 100644 --- a/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContactImplTest.java +++ b/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/EvcsKebaKeContactImplTest.java @@ -1,8 +1,8 @@ package io.openems.edge.evcs.keba.kecontact; import static io.openems.edge.evcs.api.PhaseRotation.L2_L3_L1; -import static io.openems.edge.evcs.api.Status.CHARGING_REJECTED; -import static io.openems.edge.evcs.keba.kecontact.Plug.PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED; +import static io.openems.edge.evcs.keba.kecontact.R2Plug.PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED; +import static io.openems.edge.evcs.keba.kecontact.R2State.INTERRUPTED; import org.junit.Test; @@ -42,10 +42,10 @@ public void test() throws Exception { .next(new TestCase() // .onBeforeProcessImage(() -> rh.accept(REPORT_2)) // - .output(EvcsKebaKeContact.ChannelId.STATUS_KEBA, CHARGING_REJECTED) // + .output(EvcsKebaKeContact.ChannelId.R2_STATE, INTERRUPTED) // .output(EvcsKebaKeContact.ChannelId.ERROR_1, 0) // .output(EvcsKebaKeContact.ChannelId.ERROR_2, 0) // - .output(EvcsKebaKeContact.ChannelId.PLUG, PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED) // + .output(EvcsKebaKeContact.ChannelId.R2_PLUG, PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED) // .output(EvcsKebaKeContact.ChannelId.ENABLE_SYS, false) // .output(EvcsKebaKeContact.ChannelId.ENABLE_USER, false) // .output(EvcsKebaKeContact.ChannelId.MAX_CURR_PERCENT, 1_000) // diff --git a/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/ReadHandlerTest.java b/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/ReadHandlerTest.java new file mode 100644 index 00000000000..f54d9e5d28c --- /dev/null +++ b/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/ReadHandlerTest.java @@ -0,0 +1,39 @@ +package io.openems.edge.evcs.keba.kecontact; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +public class ReadHandlerTest { + + @Test + public void testEnergySessionHandler() { + var sut = new ReadHandler.EnergySessionHandler(); + sut.updatePlug(R2Plug.UNDEFINED); + assertNull(sut.updateFromReport3(null)); + + assertEquals(990, sut.updateFromReport3(990).intValue()); + + sut.updatePlug(R2Plug.PLUGGED_ON_EVCS_AND_ON_EV); + assertEquals(1000, sut.updateFromReport3(1000).intValue()); + + sut.updatePlug(R2Plug.UNPLUGGED); + assertNull(sut.updateFromReport3(1010)); + + sut.updatePlug(R2Plug.PLUGGED_ON_EVCS_AND_ON_EV); + assertNull(sut.updateFromReport3(1020)); + + sut.updatePlug(R2Plug.PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED); + assertEquals(500, sut.updateFromReport3(500).intValue()); + + sut.updatePlug(R2Plug.UNPLUGGED); + assertNull(sut.updateFromReport3(510)); + + sut.updatePlug(R2Plug.PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED); + assertNull(sut.updateFromReport3(510)); + + sut.updatePlug(R2Plug.PLUGGED_ON_EVCS_AND_ON_EV_AND_LOCKED); + assertEquals(20, sut.updateFromReport3(20).intValue()); + } +} diff --git a/io.openems.edge.io.weidmueller/src/io/openems/edge/io/weidmueller/IoWeidmuellerUr20.java b/io.openems.edge.io.weidmueller/src/io/openems/edge/io/weidmueller/IoWeidmuellerUr20.java index 05ed735239d..d4e9044d18d 100644 --- a/io.openems.edge.io.weidmueller/src/io/openems/edge/io/weidmueller/IoWeidmuellerUr20.java +++ b/io.openems.edge.io.weidmueller/src/io/openems/edge/io/weidmueller/IoWeidmuellerUr20.java @@ -20,4 +20,4 @@ public Doc doc() { } } -} +} \ No newline at end of file diff --git a/io.openems.edge.io.weidmueller/src/io/openems/edge/io/weidmueller/IoWeidmuellerUr20Impl.java b/io.openems.edge.io.weidmueller/src/io/openems/edge/io/weidmueller/IoWeidmuellerUr20Impl.java index 1ac395603ea..159528ebcf8 100644 --- a/io.openems.edge.io.weidmueller/src/io/openems/edge/io/weidmueller/IoWeidmuellerUr20Impl.java +++ b/io.openems.edge.io.weidmueller/src/io/openems/edge/io/weidmueller/IoWeidmuellerUr20Impl.java @@ -152,6 +152,18 @@ private void activate(ComponentContext context, Config config) throws OpenemsExc tasks = myTasks.toArray(Task[]::new); break; } + + case UR20_16DI_P: { + var element = new BitsWordElement(inputRegisterOffset, this); + for (var i = 0; i < 16; i++) { + var channelId = FieldbusChannelId.forDigitalInput(moduleCount, i + 1); + var channel = (BooleanReadChannel) this.addChannel(channelId); + this.modules.get(module).add(channel); + element.bit(i, channelId); + } + tasks = new Task[] { new FC3ReadRegistersTask(element.startAddress, Priority.HIGH, element) }; + break; + } } if (tasks == null || tasks.length == 0) { @@ -237,4 +249,4 @@ private CompletableFuture> readCurrentModuleList(int numberOfEntries) .thenApply(rsr -> ((ReadElementsResult) rsr).values()); } -} +} \ No newline at end of file diff --git a/io.openems.edge.io.weidmueller/src/io/openems/edge/io/weidmueller/URemoteModule.java b/io.openems.edge.io.weidmueller/src/io/openems/edge/io/weidmueller/URemoteModule.java index cddc3bc8d71..7f4ea68c666 100644 --- a/io.openems.edge.io.weidmueller/src/io/openems/edge/io/weidmueller/URemoteModule.java +++ b/io.openems.edge.io.weidmueller/src/io/openems/edge/io/weidmueller/URemoteModule.java @@ -12,7 +12,7 @@ public enum URemoteModule { // UR20_8DI_P_2W(0x00131FC1), // // UR20_8DI_P_3W(0x000A1FC1), // // UR20_8DI_P_3W_HD(0x00031FC1), // - // UR20_16DI_P(0x00049FC2), // + UR20_16DI_P(0x00049FC2, 2, 0), // // UR20_16DI_P_PLC_INT(0x00059FC2), // // UR20_2DI_P_TS(0x0F014700), // // UR20_4DI_P_TS(0x0F024700), // diff --git a/io.openems.edge.io.weidmueller/test/io/openems/edge/io/weidmueller/IoWeidmuellerUr20ImplTest.java b/io.openems.edge.io.weidmueller/test/io/openems/edge/io/weidmueller/IoWeidmuellerUr20ImplTest.java index eadbf247bb5..19e62d5be1c 100644 --- a/io.openems.edge.io.weidmueller/test/io/openems/edge/io/weidmueller/IoWeidmuellerUr20ImplTest.java +++ b/io.openems.edge.io.weidmueller/test/io/openems/edge/io/weidmueller/IoWeidmuellerUr20ImplTest.java @@ -21,4 +21,4 @@ public void test() throws Exception { .next(new TestCase()); } -} +} \ No newline at end of file diff --git a/ui/src/app/edge/history/Controller/ModbusTcpApi/overview/overview.html b/ui/src/app/edge/history/Controller/ModbusTcpApi/overview/overview.html index c08da36f604..db62436e5d5 100644 --- a/ui/src/app/edge/history/Controller/ModbusTcpApi/overview/overview.html +++ b/ui/src/app/edge/history/Controller/ModbusTcpApi/overview/overview.html @@ -1,23 +1,23 @@

- - - - - - - Edge.Index.Widgets.InfoStorageForCharge - - - - - Edge.Index.Widgets.InfoStorageForDischarge - - - - - -
- + + + + + + + Edge.Index.Widgets.InfoStorageForCharge + + + + + Edge.Index.Widgets.InfoStorageForDischarge + + + + + + + diff --git a/ui/src/app/edge/history/storage/storagechartoverview/storagechartoverview.component.html b/ui/src/app/edge/history/storage/storagechartoverview/storagechartoverview.component.html index e145a52ca1e..4c6b7d56044 100644 --- a/ui/src/app/edge/history/storage/storagechartoverview/storagechartoverview.component.html +++ b/ui/src/app/edge/history/storage/storagechartoverview/storagechartoverview.component.html @@ -43,12 +43,12 @@ - + Edge.Index.Widgets.InfoStorageForCharge - + Edge.Index.Widgets.InfoStorageForDischarge @@ -75,12 +75,13 @@ - + Edge.Index.Widgets.InfoStorageForCharge - + Edge.Index.Widgets.InfoStorageForDischarge @@ -119,12 +120,13 @@ - + Edge.Index.Widgets.InfoStorageForCharge - + Edge.Index.Widgets.InfoStorageForDischarge diff --git a/ui/src/app/edge/live/Controller/ModbusTcpApi/modal/modal.ts b/ui/src/app/edge/live/Controller/ModbusTcpApi/modal/modal.ts index c4a8ef62502..e55c75f18a4 100644 --- a/ui/src/app/edge/live/Controller/ModbusTcpApi/modal/modal.ts +++ b/ui/src/app/edge/live/Controller/ModbusTcpApi/modal/modal.ts @@ -47,8 +47,8 @@ export class ModalComponent extends AbstractModal { }); } - protected getModbusProtocol(componentId: string) { - return this.profile.getModbusProtocol(componentId); + protected getModbusProtocol(componentId: string, type: string) { + return this.profile.getModbusProtocol(componentId, type); } protected override onCurrentData(currentData: CurrentData) { diff --git a/ui/src/app/edge/settings/app/formly/safe-input/formly-safe-input-modal.component.html b/ui/src/app/edge/settings/app/formly/safe-input/formly-safe-input-modal.component.html index 56855d3e69d..137672073ef 100644 --- a/ui/src/app/edge/settings/app/formly/safe-input/formly-safe-input-modal.component.html +++ b/ui/src/app/edge/settings/app/formly/safe-input/formly-safe-input-modal.component.html @@ -10,7 +10,7 @@
- + diff --git a/ui/src/app/edge/settings/profile/profile.component.html b/ui/src/app/edge/settings/profile/profile.component.html index 9cde0096885..388f2734929 100644 --- a/ui/src/app/edge/settings/profile/profile.component.html +++ b/ui/src/app/edge/settings/profile/profile.component.html @@ -98,21 +98,27 @@

{{ item.alias }} - Download Protocol + Download Protocol General.manual + + Download Protocol + - Download Protocol + Download Protocol General.manual + + Download Protocol + diff --git a/ui/src/app/edge/settings/profile/profile.component.ts b/ui/src/app/edge/settings/profile/profile.component.ts index cbc086d4715..ebd165df44e 100644 --- a/ui/src/app/edge/settings/profile/profile.component.ts +++ b/ui/src/app/edge/settings/profile/profile.component.ts @@ -46,11 +46,11 @@ export class ProfileComponent implements OnInit { }); } - public getModbusProtocol(componentId: string) { + public getModbusProtocol(componentId: string, type: string) { this.service.getCurrentEdge().then(edge => { const request = new ComponentJsonApiRequest({ componentId: componentId, payload: new GetModbusProtocolExportXlsxRequest() }); edge.sendRequest(this.service.websocket, request).then(response => { - Utils.downloadXlsx(response as Base64PayloadResponse, "Modbus-TCP-" + edge.id); + Utils.downloadXlsx(response as Base64PayloadResponse, "Modbus-" + type + "-" + edge.id); }).catch(reason => { this.service.toast(this.translate.instant("Edge.Config.PROFILE.ERROR_DOWNLOADING_MODBUS_PROTOCOL") + ": " + (reason as JsonrpcResponseError).error.message, "danger"); }); diff --git a/ui/src/app/index/login.component.ts b/ui/src/app/index/login.component.ts index 8c738c710ba..09b6ef14b49 100644 --- a/ui/src/app/index/login.component.ts +++ b/ui/src/app/index/login.component.ts @@ -19,16 +19,19 @@ import { UserComponent } from "../user/user.component"; standalone: false, }) export class LoginComponent implements ViewWillEnter, AfterContentChecked, OnDestroy, OnInit { + public currentThemeMode: string; public environment = environment; public form: FormGroup; protected formIsDisabled: boolean = false; protected popoverActive: "android" | "ios" | null = null; + protected showPassword: boolean = false; protected readonly operatingSystem = AppService.deviceInfo.os; protected readonly isApp: boolean = Capacitor.getPlatform() !== "web"; private stopOnDestroy: Subject = new Subject(); private page = 0; + constructor( public service: Service, public websocket: Websocket, @@ -163,4 +166,5 @@ export class LoginComponent implements ViewWillEnter, AfterContentChecked, OnDes this.popoverActive = operatingSystem; } } + } diff --git a/ui/src/app/shared/components/edge/edgeconfig.spec.ts b/ui/src/app/shared/components/edge/edgeconfig.spec.ts index e4d7ecfc6ec..1a94c6d8305 100644 --- a/ui/src/app/shared/components/edge/edgeconfig.spec.ts +++ b/ui/src/app/shared/components/edge/edgeconfig.spec.ts @@ -197,6 +197,18 @@ export namespace DummyConfig { "io.openems.edge.timedata.api.TimedataProvider", ], }; + + export const MODBUS_RTU_READWRITE = { + id: "Controller.Api.ModbusRtu.ReadWrite", + natureIds: [ + "io.openems.edge.common.jsonapi.JsonApi", + "io.openems.edge.common.component.OpenemsComponent", + "io.openems.edge.controller.api.modbus.ModbusRtuApi", + "io.openems.edge.controller.api.modbus.readwrite.ControllerApiModbusRtuReadWrite", + "io.openems.edge.controller.api.Controller", + "io.openems.edge.timedata.api.TimedataProvider", + ], + }; } export namespace Component { diff --git a/ui/src/app/shared/components/edge/edgeconfig.ts b/ui/src/app/shared/components/edge/edgeconfig.ts index 0ddf42d24ca..92412cc06ae 100644 --- a/ui/src/app/shared/components/edge/edgeconfig.ts +++ b/ui/src/app/shared/components/edge/edgeconfig.ts @@ -183,6 +183,8 @@ export class EdgeConfig { "Controller.Api.ModbusTcp", "Controller.Api.ModbusTcp.ReadOnly", "Controller.Api.ModbusTcp.ReadWrite", + "Controller.Api.ModbusRtu.ReadOnly", + "Controller.Api.ModbusRtu.ReadWrite", "Controller.Api.MQTT", "Controller.Api.Rest.ReadOnly", "Controller.Api.Rest.ReadWrite", diff --git a/ui/src/app/shared/components/formly/form-field.wrapper.ts b/ui/src/app/shared/components/formly/form-field.wrapper.ts index 71ea84f9036..94410ddbb8d 100644 --- a/ui/src/app/shared/components/formly/form-field.wrapper.ts +++ b/ui/src/app/shared/components/formly/form-field.wrapper.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component } from "@angular/core"; +import { ChangeDetectionStrategy, Component, ViewEncapsulation } from "@angular/core"; import { FieldWrapper } from "@ngx-formly/core"; @Component({ @@ -6,12 +6,12 @@ import { FieldWrapper } from "@ngx-formly/core"; templateUrl: "./form-field.wrapper.html", changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, + encapsulation: ViewEncapsulation.None, styles: [` - :host { - formly-field-ion-toggle, formly-field-ion-checkbox{ - width: 100%; - } - } + formly-field-ion-toggle, formly-field-ion-checkbox, formly-custom-select, + formly-input-serial-number { + width: 100%; + } `], }) export class FormlyWrapperFormFieldComponent extends FieldWrapper { } diff --git a/ui/src/app/shared/components/formly/formly-select/formly-select.ts b/ui/src/app/shared/components/formly/formly-select/formly-select.ts new file mode 100644 index 00000000000..daf30a3f137 --- /dev/null +++ b/ui/src/app/shared/components/formly/formly-select/formly-select.ts @@ -0,0 +1,33 @@ +import { Component } from "@angular/core"; +import { FieldType } from "@ngx-formly/core"; + +@Component({ + selector: "formly-custom-select", + template: ` + + + + {{ option.label }} + + + + `, + standalone: false, + styles: [` + :host { + formly-custom-select { + width: 100%; + } + } + `], +}) +export class FormlySelectComponent extends FieldType { } diff --git a/ui/src/app/shared/components/formly/input-serial-number-wrapper.html b/ui/src/app/shared/components/formly/input-serial-number-wrapper.html index 169d30f12e8..57459c47597 100644 --- a/ui/src/app/shared/components/formly/input-serial-number-wrapper.html +++ b/ui/src/app/shared/components/formly/input-serial-number-wrapper.html @@ -1,23 +1,24 @@ - - - - - {{to.label}} - * -
{{ to.description }}
-
-
-
- - - {{to.prefix}} - + + + + + +
+ + {{ props.label }} + * +
{{ props.description + }}
+
+
+
- - + + diff --git a/ui/src/app/shared/components/formly/input.html b/ui/src/app/shared/components/formly/input.html index 49f9db44c50..5d8f34cf7e9 100644 --- a/ui/src/app/shared/components/formly/input.html +++ b/ui/src/app/shared/components/formly/input.html @@ -4,12 +4,13 @@ + labelPlacement="start" style="text-align: right">
- - {{ to.label }} - * -
{{ to.description }}
+ + {{ props.label }} + * +
{{ props.description + }}
diff --git a/ui/src/app/shared/components/formly/input.ts b/ui/src/app/shared/components/formly/input.ts index 4b3dcaf4647..71642277be9 100644 --- a/ui/src/app/shared/components/formly/input.ts +++ b/ui/src/app/shared/components/formly/input.ts @@ -1,10 +1,11 @@ -import { Component } from "@angular/core"; +import { Component, ViewEncapsulation } from "@angular/core"; import { FieldType } from "@ngx-formly/core"; @Component({ selector: "formly-input-section", templateUrl: "./input.html", standalone: false, + encapsulation: ViewEncapsulation.None, styles: [` :host { min-width: fit-content; diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts index c4ac3d7797c..62443764f46 100644 --- a/ui/src/app/shared/shared.module.ts +++ b/ui/src/app/shared/shared.module.ts @@ -20,6 +20,7 @@ import { FormlyWrapperFormFieldComponent } from "./components/formly/form-field. import { FormlyFieldCheckboxWithImageComponent } from "./components/formly/formly-field-checkbox-image/formly-field-checkbox-with-image"; import { FormlyFieldModalComponent } from "./components/formly/formly-field-modal/formlyfieldmodal"; import { FormlyFieldRadioWithImageComponent } from "./components/formly/formly-field-radio-with-image/formly-field-radio-with-image"; +import { FormlySelectComponent } from "./components/formly/formly-select/formly-select"; import { FormlySelectFieldModalComponent } from "./components/formly/formly-select-field-modal.component"; import { FormlySelectFieldExtendedWrapperComponent } from "./components/formly/formly-select-field.extended"; import { FormlyFieldWithLoadingAnimationComponent } from "./components/formly/formly-skeleton-wrapper"; @@ -79,6 +80,7 @@ export function SubnetmaskValidatorMessage(err, field: FormlyFieldConfig) { { name: "input", component: InputTypeComponent }, { name: "repeat", component: RepeatTypeComponent }, { name: "multi-step", component: FormlyFieldMultiStepComponent }, + { name: "select", component: FormlySelectComponent }, ], validators: [ { name: "ip", validation: IpValidator }, @@ -121,6 +123,7 @@ export function SubnetmaskValidatorMessage(err, field: FormlyFieldConfig) { PanelWrapperComponent, PercentageBarComponent, RepeatTypeComponent, + FormlySelectComponent, ], exports: [ AppHeaderComponent, diff --git a/ui/src/global.scss b/ui/src/global.scss index 50915dddbaa..d27e8c37c1c 100644 --- a/ui/src/global.scss +++ b/ui/src/global.scss @@ -47,6 +47,22 @@ ion-card-content { color: var(--ion-card-content-color); } +input:-webkit-autofill, +input:-webkit-autofill:focus { + background-color: transparent !important; + box-shadow: 0 0 0em 1000em var(--ion-background-color) inset !important; + -webkit-text-fill-color: var(--ion-text-color) !important; + margin-top: 0.1em; +} + +.custom-ion-popover { + + white-space: inherit; + + // Overwrites default width of popover + --min-width: min-content; +} + .footer-color { background-color: var(--ion-color-toolbar-secondary) !important; }