diff --git a/io.openems.common/src/io/openems/common/utils/EnumUtils.java b/io.openems.common/src/io/openems/common/utils/EnumUtils.java index cbbd7573574..fecb288370e 100644 --- a/io.openems.common/src/io/openems/common/utils/EnumUtils.java +++ b/io.openems.common/src/io/openems/common/utils/EnumUtils.java @@ -48,6 +48,24 @@ public static > Optional getAsOptionalString(Enu } } + /** + * Gets the member of the {@link EnumMap} as {@link Optional} {@link Integer}. + * + * @param the type of the EnumMap key + * @param map the {@link EnumMap} + * @param member the member + * @return the {@link Optional} {@link Integer} value + * @throws OpenemsNamedException on error + */ + public static > Optional getAsOptionalInt(EnumMap map, + ENUM member) { + try { + return Optional.of(getAsInt(map, member)); + } catch (OpenemsNamedException e) { + return Optional.empty(); + } + } + /** * Gets the member of the {@link EnumMap} as {@link JsonPrimitive}. * 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 208e6542526..21cbaec8005 100644 --- a/io.openems.common/src/io/openems/common/utils/JsonUtils.java +++ b/io.openems.common/src/io/openems/common/utils/JsonUtils.java @@ -5,12 +5,19 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Optional; +import java.util.Set; import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; import java.util.function.Consumer; +import java.util.stream.Collector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +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; @@ -291,8 +298,49 @@ public JsonObject build() { } + public static class JsonArrayCollector implements Collector { + + @Override + public Set characteristics() { + return Sets.newHashSet().stream().collect(Sets.toImmutableEnumSet()); + } + + @Override + public Supplier supplier() { + return JsonUtils::buildJsonArray; + } + + @Override + public BiConsumer accumulator() { + return JsonUtils.JsonArrayBuilder::add; + } + + @Override + public BinaryOperator combiner() { + return (t, u) -> { + u.build().forEach(j -> t.add(j)); + return t; + }; + } + + @Override + public Function finisher() { + return JsonArrayBuilder::build; + } + + } + private static final Logger LOG = LoggerFactory.getLogger(JsonUtils.class); + /** + * Returns a Collector that accumulates the input elements into a new JsonArray. + * + * @return a Collector which collects all the input elements into a JsonArray + */ + public static Collector toJsonArray() { + return new JsonUtils.JsonArrayCollector(); + } + /** * Creates a JsonArray using a Builder. * 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 2a7c8c16f14..bca4521ec06 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 @@ -1,21 +1,29 @@ package io.openems.edge.common.test; +import java.io.IOException; 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.concurrent.CompletableFuture; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.openems.common.exceptions.OpenemsError; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.jsonrpc.base.GenericJsonrpcResponseSuccess; import io.openems.common.jsonrpc.base.JsonrpcRequest; import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.common.jsonrpc.request.GetEdgeConfigRequest; +import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest; import io.openems.common.jsonrpc.response.GetEdgeConfigResponse; import io.openems.common.session.Role; import io.openems.common.types.EdgeConfig; @@ -33,6 +41,8 @@ public class DummyComponentManager implements ComponentManager { private final Clock clock; private JsonObject edgeConfigJson; + private ConfigurationAdmin configurationAdmin = null; + public DummyComponentManager() { this(Clock.systemDefaultZone()); } @@ -56,7 +66,7 @@ public List getAllComponents() { public List getEnabledComponentsOfType(Class clazz) { List result = new ArrayList<>(); for (OpenemsComponent component : this.components) { - if (component.getClass().isInstance(clazz)) { + if (clazz.isInstance(component)) { result.add((T) component); } } @@ -167,6 +177,8 @@ public CompletableFuture handleJsonrpcRequest(User user, case GetEdgeConfigRequest.METHOD: return this.handleGetEdgeConfigRequest(user, GetEdgeConfigRequest.from(request)); + case UpdateComponentConfigRequest.METHOD: + return this.handleUpdateComponentConfigRequest(user, UpdateComponentConfigRequest.from(request)); default: throw OpenemsError.JSONRPC_UNHANDLED_METHOD.exception(request.getMethod()); @@ -188,9 +200,33 @@ private CompletableFuture handleGetEdgeConfigRequest(Use return CompletableFuture.completedFuture(response); } + private CompletableFuture handleUpdateComponentConfigRequest(User user, + UpdateComponentConfigRequest request) throws OpenemsNamedException { + if (this.configurationAdmin == null) { + throw new OpenemsException("Can not update Component Config. ConfigurationAdmin is null!"); + } + try { + for (var configuration : this.configurationAdmin.listConfigurations(request.getComponentId())) { + var properties = new Hashtable(); + for (var property : request.getProperties()) { + properties.put(property.getName(), property.getValue()); + } + configuration.update(properties); + break; + } + return CompletableFuture.completedFuture(new GenericJsonrpcResponseSuccess(request.getId())); + } catch (IOException | InvalidSyntaxException e) { + throw new OpenemsException("Can not update Component Config."); + } + } + @Override public Clock getClock() { return this.clock; } + public void setConfigurationAdmin(ConfigurationAdmin configurationAdmin) { + this.configurationAdmin = configurationAdmin; + } + } \ No newline at end of file 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 99f26c0203a..2b7c020688d 100644 --- a/io.openems.edge.core/.settings/org.eclipse.core.resources.prefs +++ b/io.openems.edge.core/.settings/org.eclipse.core.resources.prefs @@ -1,2 +1,4 @@ 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/=UTF-8 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 71ddea1f242..c3b40ce4144 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 @@ -2,7 +2,6 @@ import java.util.EnumMap; import java.util.List; -import java.util.Map; import java.util.TreeMap; import org.osgi.service.cm.ConfigurationAdmin; @@ -14,7 +13,8 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.api.ModbusTcpApiReadOnly.Property; @@ -29,8 +29,7 @@ import io.openems.edge.core.appmanager.OpenemsAppCardinality; import io.openems.edge.core.appmanager.OpenemsAppCategory; import io.openems.edge.core.appmanager.validator.CheckAppsNotInstalled; -import io.openems.edge.core.appmanager.validator.Validator; -import io.openems.edge.core.appmanager.validator.Validator.Builder; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** * Describes a App for ReadOnly Modbus/TCP Api. @@ -45,8 +44,7 @@ "CONTROLLER_ID": "ctrlApiModbusTcp0" }, "appDescriptor": { - "websiteUrl": https://docs.fenecon.de/de/_/latest/fems/apis.html#_fems_app_modbustcp_api_lesend + "websiteUrl": URL } } * @@ -66,8 +64,8 @@ public ModbusTcpApiReadOnly(@Reference ComponentManager componentManager, Compon } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + return AppAssistant.create(this.getName(language)) // .build(); } @@ -82,29 +80,19 @@ public OpenemsAppCategory[] getCategorys() { return new OpenemsAppCategory[] { OpenemsAppCategory.API }; } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "Modbus/TCP-Api Read-Only"; - } - @Override public OpenemsAppCardinality getCardinality() { return OpenemsAppCardinality.SINGLE; } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { var controllerId = this.getId(t, p, Property.CONTROLLER_ID, "ctrlApiModbusTcp0"); List components = Lists.newArrayList(// - new EdgeConfig.Component(controllerId, this.getName(), "Controller.Api.ModbusTcp.ReadOnly", + new EdgeConfig.Component(controllerId, this.getName(l), "Controller.Api.ModbusTcp.ReadOnly", JsonUtils.buildJsonObject() // .build())); @@ -113,14 +101,13 @@ protected ThrowingBiFunction } @Override - public Builder getValidateBuilder() { - return Validator.create() // - .setInstallableCheckableNames(new Validator.MapBuilder<>(new TreeMap>()) // - .put(CheckAppsNotInstalled.COMPONENT_NAME, // - new Validator.MapBuilder<>(new TreeMap()) // + protected io.openems.edge.core.appmanager.validator.ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setInstallableCheckableConfigs( + Lists.newArrayList(new ValidatorConfig.CheckableConfig(CheckAppsNotInstalled.COMPONENT_NAME, + new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("appIds", new String[] { "App.Api.ModbusTcp.ReadWrite" }) // - .build()) - .build()); + .build()))); } @Override 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 37f65e16398..bc7ed3c9175 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 @@ -2,7 +2,6 @@ import java.util.EnumMap; import java.util.List; -import java.util.Map; import java.util.TreeMap; import org.osgi.service.cm.ConfigurationAdmin; @@ -14,7 +13,8 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.EnumUtils; import io.openems.common.utils.JsonUtils; @@ -32,10 +32,9 @@ 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.validator.CheckAppsNotInstalled; -import io.openems.edge.core.appmanager.validator.CheckNoComponentInstalledOfFactoryId; -import io.openems.edge.core.appmanager.validator.Validator; -import io.openems.edge.core.appmanager.validator.Validator.Builder; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** * Describes a App for ReadWrite Modbus/TCP Api. @@ -52,8 +51,7 @@ "COMPONENT_IDS": ["_sum", ...] }, "appDescriptor": { - "websiteUrl": https://fenecon.de/fems-2-2/fems-app-modbus-tcp-schreibzugriff-2/ + "websiteUrl": URL } } * @@ -76,12 +74,14 @@ public ModbusTcpApiReadWrite(@Reference ComponentManager componentManager, Compo } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildInput(Property.API_TIMEOUT) // - .setLabel("Api-Timeout") // - .setDescription("Sets the timeout in seconds for updates on Channels set by this Api.") + .setLabel(TranslationUtil.getTranslation(bundle, "App.Api.apiTimeout.label")) // + .setDescription( + TranslationUtil.getTranslation(bundle, "App.Api.apiTimeout.description")) // .setDefaultValue(60) // .isRequired(true) // .setInputType(Type.NUMBER) // @@ -91,11 +91,13 @@ public AppAssistant getAppAssistant() { .add(JsonFormlyUtil.buildSelect(Property.COMPONENT_IDS) // .isMulti(true) // .isRequired(true) // - .setLabel("Component-IDs") // - .setDescription("Components that should be made available via Modbus.") + .setLabel( + TranslationUtil.getTranslation(bundle, this.getAppId() + ".componentIds.label")) // + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".componentIds.description")) // .setOptions(this.componentManager.getAllComponents(), t -> t.id() + ": " + t.alias(), OpenemsComponent::id) - .setDefaultValue("_sum") // + .setDefaultValue(JsonUtils.buildJsonArray().add("_sum").build()) // .build()) .build()) .build(); @@ -112,24 +114,14 @@ public OpenemsAppCategory[] getCategorys() { return new OpenemsAppCategory[] { OpenemsAppCategory.API }; } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "Modbus/TCP-Api Read-Write"; - } - @Override public OpenemsAppCardinality getCardinality() { return OpenemsAppCardinality.SINGLE; } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { var controllerId = this.getId(t, p, Property.CONTROLLER_ID, "ctrlApiModbusTcp0"); var apiTimeout = EnumUtils.getAsInt(p, Property.API_TIMEOUT); @@ -144,7 +136,7 @@ protected ThrowingBiFunction } List components = Lists.newArrayList(// - new EdgeConfig.Component(controllerId, this.getName(), "Controller.Api.ModbusTcp.ReadWrite", + new EdgeConfig.Component(controllerId, this.getName(l), "Controller.Api.ModbusTcp.ReadWrite", JsonUtils.buildJsonObject() // .addProperty("apiTimeout", apiTimeout) // .add("component.ids", controllerIds).build())); @@ -154,20 +146,13 @@ protected ThrowingBiFunction } @Override - public Builder getValidateBuilder() { - return Validator.create() // - .setInstallableCheckableNames(new Validator.MapBuilder<>(new TreeMap>()) // - .put(CheckAppsNotInstalled.COMPONENT_NAME, // - new Validator.MapBuilder<>(new TreeMap()) // + protected io.openems.edge.core.appmanager.validator.ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setInstallableCheckableConfigs( + Lists.newArrayList(new ValidatorConfig.CheckableConfig(CheckAppsNotInstalled.COMPONENT_NAME, + new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("appIds", new String[] { "App.Api.ModbusTcp.ReadOnly" }) // - .build()) - // TODO remove this if the free apps get created via App-Manager and an actual - // app instance gets created - .put(CheckNoComponentInstalledOfFactoryId.COMPONENT_NAME, // - new Validator.MapBuilder<>(new TreeMap()) // - .put("factorieId", "Controller.Api.ModbusTcp.ReadOnly") // - .build()) - .build()); + .build()))); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/MqttApi.java b/io.openems.edge.core/src/io/openems/edge/app/api/MqttApi.java index ec973bbc76c..becd05b419c 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/api/MqttApi.java +++ b/io.openems.edge.core/src/io/openems/edge/app/api/MqttApi.java @@ -11,7 +11,8 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.EnumUtils; import io.openems.common.utils.JsonUtils; @@ -28,6 +29,7 @@ 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; /** * Describes a App for MQTT Api. @@ -45,11 +47,13 @@ "CLIENT_ID": "edge0", "URI": "tcp://localhost:1883" }, - "appDescriptor": {} + "appDescriptor": { + "websiteUrl": URL + } } * */ -@org.osgi.service.component.annotations.Component(name = "App.Api.Mqtt.ReadWrite") +@org.osgi.service.component.annotations.Component(name = "App.Api.Mqtt") public class MqttApi extends AbstractOpenemsApp implements OpenemsApp { public static enum Property { @@ -69,31 +73,36 @@ public MqttApi(@Reference ComponentManager componentManager, ComponentContext co } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildInput(Property.USERNAME) // - .setDescription("Username for authentication at MQTT broker.") // - .setLabel("Username") // + .setLabel(TranslationUtil.getTranslation(bundle, "username")) // + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".Username.description")) // .isRequired(true) // .setMinLenght(3) // .setMaxLenght(18) // .build()) // .add(JsonFormlyUtil.buildInput(Property.PASSWORD) // - .setLabel("Password") // - .setDescription("Password for authentication at MQTT broker.") // + .setLabel(TranslationUtil.getTranslation(bundle, "password")) // + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".Password.description")) // .isRequired(true) // .setInputType(Type.PASSWORD) // .build()) // .add(JsonFormlyUtil.buildInput(Property.CLIENT_ID) // - .setLabel("Client-ID") // - .setDescription("Client-ID for authentication at MQTT broker.") // + .setLabel(TranslationUtil.getTranslation(bundle, this.getAppId() + ".EdgeId.label")) // + .setDescription( + TranslationUtil.getTranslation(bundle, this.getAppId() + ".EdgeId.description")) // .setDefaultValue("edge0") // .isRequired(true) // .build()) .add(JsonFormlyUtil.buildInput(Property.URI) // .setLabel("Uri") // - .setDescription("The connection Uri to MQTT broker.") // + .setDescription( + TranslationUtil.getTranslation(bundle, this.getAppId() + ".Uri.description")) // .setDefaultValue("tcp://localhost:1883") // .isRequired(true) // .build()) // @@ -112,24 +121,14 @@ public OpenemsAppCategory[] getCategorys() { return new OpenemsAppCategory[] { OpenemsAppCategory.API }; } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "MQTT-Api"; - } - @Override public OpenemsAppCardinality getCardinality() { return OpenemsAppCardinality.SINGLE; } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { var clientId = this.getValueOrDefault(p, Property.CLIENT_ID, "edge0"); var uri = this.getValueOrDefault(p, Property.URI, "tcp://localhost:1883"); @@ -140,7 +139,7 @@ protected ThrowingBiFunction var controllerId = this.getId(t, p, Property.CONTROLLER_ID, "ctrlControllerApiMqtt0"); var components = Lists.newArrayList(// - new EdgeConfig.Component(controllerId, this.getName(), "Controller.Api.MQTT", + new EdgeConfig.Component(controllerId, this.getName(l), "Controller.Api.MQTT", JsonUtils.buildJsonObject() // .addProperty("clientId", clientId) // .addProperty("uri", uri) // diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadOnly.java b/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadOnly.java index 9f58819ebdd..f7feaca51f4 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadOnly.java +++ b/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadOnly.java @@ -2,7 +2,6 @@ import java.util.EnumMap; import java.util.List; -import java.util.Map; import java.util.TreeMap; import org.osgi.service.cm.ConfigurationAdmin; @@ -14,7 +13,8 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.api.RestJsonApiReadOnly.Property; @@ -29,8 +29,7 @@ import io.openems.edge.core.appmanager.OpenemsAppCardinality; import io.openems.edge.core.appmanager.OpenemsAppCategory; import io.openems.edge.core.appmanager.validator.CheckAppsNotInstalled; -import io.openems.edge.core.appmanager.validator.Validator; -import io.openems.edge.core.appmanager.validator.Validator.Builder; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** * Describes a App for ReadOnly Rest JSON Api. @@ -45,8 +44,7 @@ "CONTROLLER_ID": "ctrlApiRest0" }, "appDescriptor": { - "websiteUrl": https://docs.fenecon.de/de/_/latest/fems/apis.html#_fems_app_modbustcp_api_lesend + "websiteUrl": URL } } * @@ -66,8 +64,8 @@ public RestJsonApiReadOnly(@Reference ComponentManager componentManager, Compone } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + return AppAssistant.create(this.getName(language)) // .build(); } @@ -82,29 +80,18 @@ public OpenemsAppCategory[] getCategorys() { return new OpenemsAppCategory[] { OpenemsAppCategory.API }; } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "REST/JSON-Api Read-Only"; - } - @Override public OpenemsAppCardinality getCardinality() { return OpenemsAppCardinality.SINGLE; } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { - + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { var controllerId = this.getId(t, p, Property.CONTROLLER_ID, "ctrlApiRest0"); List components = Lists.newArrayList(// - new EdgeConfig.Component(controllerId, this.getName(), "Controller.Api.Rest.ReadOnly", + new EdgeConfig.Component(controllerId, this.getName(l), "Controller.Api.Rest.ReadOnly", JsonUtils.buildJsonObject() // .build())); @@ -113,18 +100,18 @@ protected ThrowingBiFunction } @Override - public Builder getValidateBuilder() { - return Validator.create() // - .setInstallableCheckableNames(new Validator.MapBuilder<>(new TreeMap>()) // - .put(CheckAppsNotInstalled.COMPONENT_NAME, // - new Validator.MapBuilder<>(new TreeMap()) // + public ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setInstallableCheckableConfigs(Lists.newArrayList(// + new ValidatorConfig.CheckableConfig(CheckAppsNotInstalled.COMPONENT_NAME, + new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("appIds", new String[] { "App.Api.RestJson.ReadWrite" }) // - .build()) - .build()); + .build()))); } @Override protected Class getPropertyClass() { return Property.class; } + } diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadWrite.java b/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadWrite.java index ed46bb583b0..4dcbc5fb4ab 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadWrite.java +++ b/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadWrite.java @@ -2,7 +2,6 @@ import java.util.EnumMap; import java.util.List; -import java.util.Map; import java.util.TreeMap; import org.osgi.service.cm.ConfigurationAdmin; @@ -14,7 +13,8 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.EnumUtils; import io.openems.common.utils.JsonUtils; @@ -31,10 +31,9 @@ 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.validator.CheckAppsNotInstalled; -import io.openems.edge.core.appmanager.validator.CheckNoComponentInstalledOfFactoryId; -import io.openems.edge.core.appmanager.validator.Validator; -import io.openems.edge.core.appmanager.validator.Validator.Builder; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** * Describes a App for ReadWrite Rest JSON Api. @@ -50,8 +49,7 @@ "API_TIMEOUT": 60 }, "appDescriptor": { - "websiteUrl": https://fenecon.de/fems-2-2/fems-app-rest-json-schreibzugriff-2/ + "websiteUrl": URL } } * @@ -73,12 +71,14 @@ public RestJsonApiReadWrite(@Reference ComponentManager componentManager, Compon } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildInput(Property.API_TIMEOUT) // - .setLabel("Api-Timeout") // - .setDescription("Sets the timeout in seconds for updates on Channels set by this Api.") + .setLabel(TranslationUtil.getTranslation(bundle, "App.Api.apiTimeout.label")) // + .setDescription( + TranslationUtil.getTranslation(bundle, "App.Api.apiTimeout.description")) // .setInputType(Type.NUMBER) // .setDefaultValue(60) // .setMin(30) // @@ -99,31 +99,20 @@ public OpenemsAppCategory[] getCategorys() { return new OpenemsAppCategory[] { OpenemsAppCategory.API }; } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "Rest/JSON-Api Read-Write"; - } - @Override public OpenemsAppCardinality getCardinality() { return OpenemsAppCardinality.SINGLE; } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { - + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { var controllerId = this.getId(t, p, Property.CONTROLLER_ID, "ctrlApiRest0"); var apiTimeout = EnumUtils.getAsInt(p, Property.API_TIMEOUT); List components = Lists.newArrayList(// - new EdgeConfig.Component(controllerId, this.getName(), "Controller.Api.Rest.ReadWrite", + new EdgeConfig.Component(controllerId, this.getName(l), "Controller.Api.Rest.ReadWrite", JsonUtils.buildJsonObject() // .addProperty("apiTimeout", apiTimeout) // .build())); @@ -133,24 +122,18 @@ protected ThrowingBiFunction } @Override - public Builder getValidateBuilder() { - return Validator.create() // - .setInstallableCheckableNames(new Validator.MapBuilder<>(new TreeMap>()) // - .put(CheckAppsNotInstalled.COMPONENT_NAME, // - new Validator.MapBuilder<>(new TreeMap()) // + public ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setInstallableCheckableConfigs(Lists.newArrayList(// + new ValidatorConfig.CheckableConfig(CheckAppsNotInstalled.COMPONENT_NAME, + new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("appIds", new String[] { "App.Api.RestJson.ReadOnly" }) // - .build()) - // TODO remove this if the free apps get created via App-Manager and an actual - // app instance gets created - .put(CheckNoComponentInstalledOfFactoryId.COMPONENT_NAME, // - new Validator.MapBuilder<>(new TreeMap()) // - .put("factorieId", "Controller.Api.Rest.ReadOnly") // - .build()) - .build()); + .build()))); } @Override protected Class getPropertyClass() { return Property.class; } + } diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsCluster.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsCluster.java index e72734cb5b8..c383fc966f1 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsCluster.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsCluster.java @@ -1,6 +1,7 @@ package io.openems.edge.app.evcs; import java.util.EnumMap; +import java.util.stream.Collectors; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -11,7 +12,8 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.EnumUtils; import io.openems.common.utils.JsonUtils; @@ -28,6 +30,7 @@ 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; /** * Describes a evcs cluster. @@ -43,8 +46,7 @@ "EVCS_IDS": [ "evcs0", "evcs1", ...] }, "appDescriptor": { - "websiteUrl": https://fenecon.de/fems-2-2/fems-app-multiladepunkt-eigenverbrauch-2/ + "websiteUrl": URL } } * @@ -66,18 +68,18 @@ public EvcsCluster(@Reference ComponentManager componentManager, ComponentContex } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { var evcsClusterId = this.getId(t, p, Property.EVCS_CLUSTER_ID, "evcsCluster0"); - var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName()); + var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); var ids = EnumUtils.getAsJsonArray(p, Property.EVCS_IDS); var components = Lists.newArrayList(new EdgeConfig.Component(evcsClusterId, alias, "Evcs.Cluster.PeakShaving", JsonUtils.buildJsonObject() // - .add("evcs.ids", ids) // + .onlyIf(t.isAddOrUpdate(), j -> j.add("evcs.ids", ids)) // .build())); return new AppConfiguration(components); @@ -85,12 +87,16 @@ protected ThrowingBiFunction } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // - .add(JsonFormlyUtil.buildSelect(Property.EVCS_IDS).setLabel("EVCS-IDs") // - .setDescription("IDs of EVCS devices.") // - .setOptions(this.componentUtil.getEnabledComponentsOfStartingId("evcs"), + .add(JsonFormlyUtil.buildSelect(Property.EVCS_IDS) // + .setLabel("EVCS-IDs") // + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".evcsIds.description")) // + .setOptions(this.componentUtil.getEnabledComponentsOfStartingId("evcs").stream() + .filter(t -> !t.id().startsWith("evcsCluster")).collect(Collectors.toList()), t -> t.alias() == null || t.alias().isEmpty() ? t.id() : t.id() + ": " + t.alias(), OpenemsComponent::id) @@ -112,16 +118,6 @@ public OpenemsAppCategory[] getCategorys() { return new OpenemsAppCategory[] { OpenemsAppCategory.EVCS }; } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "Multiladepunkt-Management"; - } - @Override protected Class getPropertyClass() { return Property.class; diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java index 71463fb0466..e0a36d5abe0 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java @@ -12,11 +12,13 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.utils.EnumUtils; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.evcs.HardyBarthEvcs.Property; import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; import io.openems.edge.core.appmanager.AppAssistant; import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDescriptor; @@ -27,6 +29,7 @@ import io.openems.edge.core.appmanager.JsonFormlyUtil.InputBuilder.Validation; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.TranslationUtil; /** * Describes a Hardy Barth evcs App. @@ -43,8 +46,7 @@ "IP":"192.168.25.30" }, "appDescriptor": { - "websiteUrl": https://fenecon.de/fems-app-echarge-hardy-barth-ladestation/ + "websiteUrl": URL } } * @@ -58,7 +60,7 @@ public static enum Property implements DefaultEnum { CTRL_EVCS_ID("ctrlEvcs0"), // IP("192.168.25.30"); - private String defaultValue; + private final String defaultValue; private Property(String defaultValue) { this.defaultValue = defaultValue; @@ -78,11 +80,11 @@ public HardyBarthEvcs(@Reference ComponentManager componentManager, ComponentCon } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { // values the user enters var ip = EnumUtils.getAsOptionalString(p, Property.IP).orElse(Property.IP.getDefaultValue()); - var alias = this.getValueOrDefault(p, Property.ALIAS); + var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); // values which are being auto generated by the appmanager var evcsId = this.getId(t, p, Property.EVCS_ID); @@ -91,18 +93,19 @@ protected ThrowingBiFunction var components = this.getComponents(evcsId, alias, "Evcs.HardyBarth", ip, ctrlEvcsId); return new AppConfiguration(components, Lists.newArrayList(ctrlEvcsId, "ctrlBalancing0"), - Lists.newArrayList("192.168.25.10/24")); + ip.startsWith("192.168.25.") ? Lists.newArrayList("192.168.25.10/24") : null); }; } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildInput(Property.IP) // - .setLabel("IP-Address") // - .setDescription("The IP address of the charging station. " - + "If the charger has two connectors, the second/slave evcs has the IP 192.168.25.31.") + .setLabel(TranslationUtil.getTranslation(bundle, "ipAddress")) // + .setDescription( + TranslationUtil.getTranslation(bundle, this.getAppId() + ".Ip.description")) .setDefaultValue(Property.IP.getDefaultValue()) // .isRequired(true) // .setValidation(Validation.IP) // @@ -117,16 +120,6 @@ public AppDescriptor getAppDescriptor() { .build(); } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "eCharge Hardy Barth Ladestation"; - } - @Override protected Class getPropertyClass() { return Property.class; diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/IesKeywattEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/IesKeywattEvcs.java new file mode 100644 index 00000000000..6c89cfcd3e7 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/IesKeywattEvcs.java @@ -0,0 +1,151 @@ +package io.openems.edge.app.evcs; + +import java.util.EnumMap; + +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 com.google.gson.JsonPrimitive; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; +import io.openems.common.utils.EnumUtils; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.evcs.IesKeywattEvcs.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AppAssistant; +import io.openems.edge.core.appmanager.AppConfiguration; +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.DefaultEnum; +import io.openems.edge.core.appmanager.JsonFormlyUtil; +import io.openems.edge.core.appmanager.JsonFormlyUtil.InputBuilder.Type; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.TranslationUtil; + +/** + * Describes a IES Keywatt evcs App. + * + *
+  {
+    "appId":"App.Evcs.IesKeywatt",
+    "alias":"IES Keywatt Ladestation",
+    "instanceId": UUID,
+    "image": base64,
+    "properties":{
+      "EVCS_ID": "evcs0",
+      "CTRL_EVCS_ID": "ctrlEvcs0",
+      "OCCP_CHARGE_POINT_IDENTIFIER":"IES 1",
+      "OCCP_CONNECTOR_IDENTIFIER": "1"
+    },
+    "appDescriptor": {
+    	"websiteUrl": URL
+    }
+  }
+ * 
+ */ +@Component(name = "App.Evcs.IesKeywatt") +public class IesKeywattEvcs extends AbstractEvcsApp implements OpenemsApp { + + public static enum Property implements DefaultEnum { + ALIAS("IES Keywatt Ladestation"), // + EVCS_ID("evcs0"), // + CTRL_EVCS_ID("ctrlEvcs0"), // + OCCP_CHARGE_POINT_IDENTIFIER("IES1"), // + OCCP_CONNECTOR_IDENTIFIER("1"), // + ; + + private final String defaultValue; + + private Property(String defaultValue) { + this.defaultValue = defaultValue; + } + + @Override + public String getDefaultValue() { + return this.defaultValue; + } + + } + + @Activate + public IesKeywattEvcs(@Reference ComponentManager componentManager, ComponentContext componentContext, + @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { + // values the user enters + var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); + + // values which are being auto generated by the appmanager + var evcsId = this.getId(t, p, Property.EVCS_ID); + var ctrlEvcsId = this.getId(t, p, Property.CTRL_EVCS_ID); + var ocppId = this.getValueOrDefault(p, Property.OCCP_CHARGE_POINT_IDENTIFIER); + + var connectorId = EnumUtils.getAsInt(p, Property.OCCP_CONNECTOR_IDENTIFIER); + + var factoryId = "Evcs.Ocpp.IesKeywattSingle"; + var components = this.getComponents(evcsId, alias, factoryId, null, ctrlEvcsId); + var evcs = AbstractOpenemsApp.getComponentWithFactoryId(components, factoryId); + evcs.getProperties().put("ocpp.id", new JsonPrimitive(ocppId)); + evcs.getProperties().put("connectorId", new JsonPrimitive(connectorId)); + + return new AppConfiguration(components, Lists.newArrayList(ctrlEvcsId, "ctrlBalancing0")); + }; + } + + @Override + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // + .fields(JsonUtils.buildJsonArray() // + .add(JsonFormlyUtil.buildInput(Property.OCCP_CHARGE_POINT_IDENTIFIER) // + .setLabel( + TranslationUtil.getTranslation(bundle, this.getAppId() + ".chargepoint.label")) // + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".chargepoint.description")) // + .setDefaultValue(Property.OCCP_CHARGE_POINT_IDENTIFIER.getDefaultValue()) // + .isRequired(true) // + .build()) // + .add(JsonFormlyUtil.buildInput(Property.OCCP_CONNECTOR_IDENTIFIER) // + .setLabel(TranslationUtil.getTranslation(bundle, this.getAppId() + ".connector.label")) // + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".connector.description")) // + .setDefaultValue(Property.OCCP_CONNECTOR_IDENTIFIER.getDefaultValue()) // + .isRequired(true) // + .setInputType(Type.NUMBER) // + .setMin(0) // + .build()) // + .build()) // + .build(); + } + + @Override + public AppDescriptor getAppDescriptor() { + return AppDescriptor.create() // + .build(); + } + + @Override + protected Class getPropertyClass() { + return Property.class; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.MULTIPLE; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java index 255a07a5f15..d94bc8cb53a 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java @@ -12,10 +12,12 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.evcs.KebaEvcs.Property; import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; import io.openems.edge.core.appmanager.AppAssistant; import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDescriptor; @@ -26,6 +28,7 @@ import io.openems.edge.core.appmanager.JsonFormlyUtil.InputBuilder.Validation; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.TranslationUtil; /** * Describes a Keba evcs App. @@ -42,8 +45,7 @@ "IP":"192.168.25.11" }, "appDescriptor": { - "websiteUrl": https://fenecon.de/fems-2-2/fems-app-keba-ladestation/ + "websiteUrl": URL } } * @@ -57,7 +59,7 @@ public enum Property implements DefaultEnum { CTRL_EVCS_ID("ctrlEvcs0"), // IP("192.168.25.11"); - private String defaultValue; + private final String defaultValue; private Property(String defaultValue) { this.defaultValue = defaultValue; @@ -77,11 +79,11 @@ public KebaEvcs(@Reference ComponentManager componentManager, ComponentContext c } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { // values the user enters var ip = this.getValueOrDefault(p, Property.IP); - var alias = this.getValueOrDefault(p, Property.ALIAS); + var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); // values which are being auto generated by the appmanager var evcsId = this.getId(t, p, Property.EVCS_ID); @@ -90,17 +92,19 @@ protected ThrowingBiFunction var components = this.getComponents(evcsId, alias, "Evcs.Keba.KeContact", ip, ctrlEvcsId); return new AppConfiguration(components, Lists.newArrayList(ctrlEvcsId, "ctrlBalancing0"), - Lists.newArrayList("192.168.25.10/24")); + ip.startsWith("192.168.25.") ? Lists.newArrayList("192.168.25.10/24") : null); }; } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildInput(Property.IP) // - .setLabel("IP-Address") // - .setDescription("The IP address of the charging station.") + .setLabel(TranslationUtil.getTranslation(bundle, "ipAddress")) // + .setDescription( + TranslationUtil.getTranslation(bundle, this.getAppId() + ".Ip.description")) .setDefaultValue(Property.IP.getDefaultValue()) // .isRequired(true) // .setValidation(Validation.IP) // @@ -115,16 +119,6 @@ public AppDescriptor getAppDescriptor() { .build(); } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "KEBA Ladestation"; - } - @Override protected Class getPropertyClass() { return Property.class; diff --git a/io.openems.edge.core/src/io/openems/edge/app/hardware/KMtronic8Channel.java b/io.openems.edge.core/src/io/openems/edge/app/hardware/KMtronic8Channel.java index aa4e2cb4196..744c03f7132 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/hardware/KMtronic8Channel.java +++ b/io.openems.edge.core/src/io/openems/edge/app/hardware/KMtronic8Channel.java @@ -12,7 +12,8 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.types.EdgeConfig.Component; import io.openems.common.utils.JsonUtils; @@ -29,6 +30,7 @@ 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; /** * Describes a App for KMtronic 8-Channel Relay. @@ -44,7 +46,9 @@ "MODBUS_ID": "modbus10", "IP": "192.168.1.199" }, - "appDescriptor": {} + "appDescriptor": { + "websiteUrl": URL + } } * */ @@ -67,10 +71,10 @@ public KMtronic8Channel(@Reference ComponentManager componentManager, ComponentC } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { - var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName()); + var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); var ip = this.getValueOrDefault(p, Property.IP, "192.168.1.199"); var modbusId = this.getId(t, p, Property.MODBUS_ID, "modbus10"); @@ -85,17 +89,20 @@ protected ThrowingBiFunction .addProperty("ip", ip) // .build())// ); - return new AppConfiguration(comp, null, Lists.newArrayList("192.168.1.198/28")); + return new AppConfiguration(comp, null, + ip.startsWith("192.168.1.") ? Lists.newArrayList("192.168.1.198/28") : null); }; } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildInput(Property.IP) // - .setLabel("IP-Address") // - .setDescription("The IP address of the Relay.") // + .setLabel(TranslationUtil.getTranslation(bundle, "ipAddress")) // + .setDescription( + TranslationUtil.getTranslation(bundle, this.getAppId() + ".Ip.description")) // .setDefaultValue("192.168.1.199") // .isRequired(true) // .setValidation(Validation.IP) // @@ -115,16 +122,6 @@ public OpenemsAppCategory[] getCategorys() { return new OpenemsAppCategory[] { OpenemsAppCategory.HARDWARE }; } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "FEMS Relais 8-Kanal"; - } - @Override protected Class getPropertyClass() { return Property.class; diff --git a/io.openems.edge.core/src/io/openems/edge/app/heat/CombinedHeatAndPower.java b/io.openems.edge.core/src/io/openems/edge/app/heat/CombinedHeatAndPower.java index 7c2e6c75b69..635f626cf8c 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/heat/CombinedHeatAndPower.java +++ b/io.openems.edge.core/src/io/openems/edge/app/heat/CombinedHeatAndPower.java @@ -3,8 +3,8 @@ import java.util.ArrayList; import java.util.EnumMap; import java.util.List; -import java.util.Map; import java.util.TreeMap; +import java.util.stream.Collectors; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -15,8 +15,8 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.exceptions.OpenemsException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.types.EdgeConfig.Component; import io.openems.common.utils.JsonUtils; @@ -29,12 +29,15 @@ import io.openems.edge.core.appmanager.ComponentUtil; import io.openems.edge.core.appmanager.ConfigurationTarget; import io.openems.edge.core.appmanager.DefaultEnum; +import io.openems.edge.core.appmanager.JsonFormlyUtil; 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.dependency.DependencyDeclaration; +import io.openems.edge.core.appmanager.dependency.DependencyUtil; import io.openems.edge.core.appmanager.validator.CheckRelayCount; -import io.openems.edge.core.appmanager.validator.Validator; -import io.openems.edge.core.appmanager.validator.Validator.Builder; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** * Describes a App for a Heating Element. @@ -46,9 +49,17 @@ "instanceId": UUID, "image": base64, "properties":{ - "CTRL_CHP_SOC_ID": "ctrlChpSoc0" + "CTRL_CHP_SOC_ID": "ctrlChpSoc0", + "OUTPUT_CHANNEL": "io0/Relay1" }, + "dependencies": [ + { + "key": "RELAY", + "instanceId": UUID + } + ], "appDescriptor": { + "websiteUrl": URL } } * @@ -59,10 +70,11 @@ public class CombinedHeatAndPower extends AbstractOpenemsApp implement public static enum Property implements DefaultEnum { // User values ALIAS("Blockheizkraftwerk"), // + OUTPUT_CHANNEL("io0/Relay1"), // // Components CTRL_CHP_SOC_ID("ctrlChpSoc0"); - private String defaultValue; + private final String defaultValue; private Property(String defaultValue) { this.defaultValue = defaultValue; @@ -82,23 +94,13 @@ public CombinedHeatAndPower(@Reference ComponentManager componentManager, Compon } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { - + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { final var bhcId = this.getId(t, p, Property.CTRL_CHP_SOC_ID); - final var alias = this.getValueOrDefault(p, Property.ALIAS); - - var outputChannelAddress = "io0/Relay1"; + final var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); + final var outputChannelAddress = this.getValueOrDefault(p, Property.OUTPUT_CHANNEL); - if (!t.isDeleteOrTest()) { - var relays = this.componentUtil.getPreferredRelays(Lists.newArrayList(bhcId), new int[] { 1 }, - new int[] { 1 }); - if (relays == null) { - throw new OpenemsException("Not enough relays available!"); - } - outputChannelAddress = relays[0]; - } List comp = new ArrayList<>(); comp.add(new EdgeConfig.Component(bhcId, alias, "Controller.CHP.SoC", JsonUtils.buildJsonObject() // @@ -108,13 +110,53 @@ protected ThrowingBiFunction .onlyIf(t == ConfigurationTarget.ADD, b -> b.addProperty("highThreshold", 80)) // .build()));// - return new AppConfiguration(comp); + var componentIdOfRelay = outputChannelAddress.substring(0, outputChannelAddress.indexOf('/')); + + var appIdOfRelay = DependencyUtil.getInstanceIdOfAppWhichHasComponent(this.componentManager, + componentIdOfRelay, this.getAppId()); + + if (appIdOfRelay == null) { + // relay may be created but not as a app + return new AppConfiguration(comp); + } + + var dependencies = Lists.newArrayList(new DependencyDeclaration("RELAY", // + DependencyDeclaration.CreatePolicy.NEVER, // + DependencyDeclaration.UpdatePolicy.NEVER, // + DependencyDeclaration.DeletePolicy.NEVER, // + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ALL, // + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED, // + DependencyDeclaration.AppDependencyConfig.create() // + .setSpecificInstanceId(appIdOfRelay) // + .build())); + + return new AppConfiguration(comp, null, null, dependencies); }; } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // + .fields(JsonUtils.buildJsonArray() // + .add(JsonFormlyUtil.buildSelect(Property.OUTPUT_CHANNEL) // + .setOptions(this.componentUtil.getAllRelays() // + .stream().map(r -> r.relays).flatMap(List::stream) // + .collect(Collectors.toList())) // + .setDefaultValueWithStringSupplier(() -> { + var relays = this.componentUtil.getPreferredRelays(Lists.newArrayList(), + new int[] { 1 }, new int[] { 1 }); + if (relays == null) { + return Property.OUTPUT_CHANNEL.getDefaultValue(); + } + return relays[0]; + }) // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannel.label")) // + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannel.description")) // + .build()) + .build()) .build(); } @@ -130,24 +172,13 @@ public OpenemsAppCategory[] getCategorys() { } @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public Builder getValidateBuilder() { - return Validator.create() // - .setInstallableCheckableNames(new Validator.MapBuilder<>(new TreeMap>()) // - .put(CheckRelayCount.COMPONENT_NAME, // - new Validator.MapBuilder<>(new TreeMap()) // + public ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setInstallableCheckableConfigs(Lists.newArrayList(// + new ValidatorConfig.CheckableConfig(CheckRelayCount.COMPONENT_NAME, + new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("count", 1) // - .build()) - .build()); - } - - @Override - public String getName() { - return "Blockheizkraftwerk (BHKW)"; + .build()))); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java index df575e1c97d..d496e952285 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java +++ b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java @@ -1,8 +1,9 @@ package io.openems.edge.app.heat; import java.util.EnumMap; -import java.util.Map; +import java.util.List; import java.util.TreeMap; +import java.util.stream.Collectors; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -13,8 +14,8 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.exceptions.OpenemsException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.heat.HeatPump.Property; @@ -25,12 +26,15 @@ 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.JsonFormlyUtil; 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.dependency.DependencyDeclaration; +import io.openems.edge.core.appmanager.dependency.DependencyUtil; import io.openems.edge.core.appmanager.validator.CheckRelayCount; -import io.openems.edge.core.appmanager.validator.Validator; -import io.openems.edge.core.appmanager.validator.Validator.Builder; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** * Describes a App for a Heat Pump. @@ -42,11 +46,18 @@ "instanceId": UUID, "image": base64, "properties":{ - "CTRL_IO_HEAT_PUMP_ID": "ctrlIoHeatPump0" + "CTRL_IO_HEAT_PUMP_ID": "ctrlIoHeatPump0", + "OUTPUT_CHANNEL_1": "io0/Relay2", + "OUTPUT_CHANNEL_2": "io0/Relay3" }, + "dependencies": [ + { + "key": "RELAY", + "instanceId": UUID + } + ], "appDescriptor": { - "websiteUrl": https://fenecon.de/fems-2-2/fems-app-sg-ready-waermepumpe-2/ + "websiteUrl": URL } } * @@ -55,7 +66,10 @@ public class HeatPump extends AbstractOpenemsApp implements OpenemsApp { public static enum Property { - CTRL_IO_HEAT_PUMP_ID; + CTRL_IO_HEAT_PUMP_ID, // + OUTPUT_CHANNEL_1, // + OUTPUT_CHANNEL_2; + } @Activate @@ -65,39 +79,70 @@ public HeatPump(@Reference ComponentManager componentManager, ComponentContext c } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { final var ctrlIoHeatPumpId = this.getId(t, p, Property.CTRL_IO_HEAT_PUMP_ID, "ctrlIoHeatPump0"); - if (t.isDeleteOrTest()) { - var comp = Lists.newArrayList(// - new EdgeConfig.Component(ctrlIoHeatPumpId, this.getName(), "Controller.Io.HeatPump.SgReady", - JsonUtils.buildJsonObject() // - .build())); - return new AppConfiguration(comp); - } - - var relays = this.componentUtil.getPreferredRelays(Lists.newArrayList(ctrlIoHeatPumpId), new int[] { 2, 3 }, - new int[] { 2, 3 }); - if (relays == null) { - throw new OpenemsException("Not enought relays available!"); - } - var outputChannel1 = relays[0]; - var outputChannel2 = relays[1]; + var outputChannel1 = this.getValueOrDefault(p, Property.OUTPUT_CHANNEL_1, "io0/Relay2"); + var outputChannel2 = this.getValueOrDefault(p, Property.OUTPUT_CHANNEL_2, "io0/Relay3"); var comp = Lists.newArrayList(// - new EdgeConfig.Component(ctrlIoHeatPumpId, this.getName(), "Controller.Io.HeatPump.SgReady", + new EdgeConfig.Component(ctrlIoHeatPumpId, this.getName(l), "Controller.Io.HeatPump.SgReady", JsonUtils.buildJsonObject() // .addProperty("outputChannel1", outputChannel1) // .addProperty("outputChannel2", outputChannel2) // .build())); - return new AppConfiguration(comp); + + var componentIdOfRelay = outputChannel1.substring(0, outputChannel1.indexOf('/')); + var appIdOfRelay = DependencyUtil.getInstanceIdOfAppWhichHasComponent(this.componentManager, + componentIdOfRelay, this.getAppId()); + + if (appIdOfRelay == null) { + // relay may be created but not as a app + return new AppConfiguration(comp); + } + + var dependencies = Lists.newArrayList(new DependencyDeclaration("RELAY", // + DependencyDeclaration.CreatePolicy.NEVER, // + DependencyDeclaration.UpdatePolicy.NEVER, // + DependencyDeclaration.DeletePolicy.NEVER, // + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ALL, // + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED, // + DependencyDeclaration.AppDependencyConfig.create() // + .setSpecificInstanceId(appIdOfRelay) // + .build())); + + return new AppConfiguration(comp, null, null, dependencies); }; } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + var relays = this.componentUtil.getPreferredRelays(Lists.newArrayList(), new int[] { 2, 3 }, + new int[] { 2, 3 }); + var options = this.componentUtil.getAllRelays() // + .stream().map(r -> r.relays).flatMap(List::stream) // + .collect(Collectors.toList()); + return AppAssistant.create(this.getName(language)) // + .fields(JsonUtils.buildJsonArray() // + .add(JsonFormlyUtil.buildSelect(Property.OUTPUT_CHANNEL_1) // + .setOptions(options) // + .onlyIf(relays != null, t -> t.setDefaultValue(relays[0])) // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannel1.label")) + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannel1.description")) + .build()) + .add(JsonFormlyUtil.buildSelect(Property.OUTPUT_CHANNEL_2) // + .setOptions(options) // + .onlyIf(relays != null, t -> t.setDefaultValue(relays[1])) // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannel2.label")) + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannel2.description")) + .build()) + .build()) .build(); } @@ -113,24 +158,13 @@ public OpenemsAppCategory[] getCategorys() { } @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public Builder getValidateBuilder() { - return Validator.create() // - .setInstallableCheckableNames(new Validator.MapBuilder<>(new TreeMap>()) // - .put(CheckRelayCount.COMPONENT_NAME, // - new Validator.MapBuilder<>(new TreeMap()) // + public ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setInstallableCheckableConfigs(Lists.newArrayList(// + new ValidatorConfig.CheckableConfig(CheckRelayCount.COMPONENT_NAME, + new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("count", 2) // - .build()) - .build()); - } - - @Override - public String getName() { - return "\"SG-Ready\" Wärmepumpe"; + .build()))); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java index 42413fda113..cd11f37bc5b 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java +++ b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java @@ -3,8 +3,8 @@ import java.util.ArrayList; import java.util.EnumMap; import java.util.List; -import java.util.Map; import java.util.TreeMap; +import java.util.stream.Collectors; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -15,8 +15,8 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.exceptions.OpenemsException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.types.EdgeConfig.Component; import io.openems.common.utils.JsonUtils; @@ -29,12 +29,15 @@ import io.openems.edge.core.appmanager.ComponentUtil; import io.openems.edge.core.appmanager.ConfigurationTarget; import io.openems.edge.core.appmanager.DefaultEnum; +import io.openems.edge.core.appmanager.JsonFormlyUtil; 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.dependency.DependencyDeclaration; +import io.openems.edge.core.appmanager.dependency.DependencyUtil; import io.openems.edge.core.appmanager.validator.CheckRelayCount; -import io.openems.edge.core.appmanager.validator.Validator; -import io.openems.edge.core.appmanager.validator.Validator.Builder; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** * Describes a App for a RTU Heating Element. @@ -46,11 +49,19 @@ "instanceId": UUID, "image": base64, "properties":{ - "CTRL_IO_HEATING_ELEMENT_ID": "ctrlIoHeatingElement0" + "CTRL_IO_HEATING_ELEMENT_ID": "ctrlIoHeatingElement0", + "OUTPUT_CHANNEL_PHASE_L1": "io0/Relay1", + "OUTPUT_CHANNEL_PHASE_L2": "io0/Relay2", + "OUTPUT_CHANNEL_PHASE_L3": "io0/Relay3" }, + "dependencies": [ + { + "key": "RELAY", + "instanceId": UUID + } + ], "appDescriptor": { - "websiteUrl": https://fenecon.de/fems-2-2/fems-app-heizstab/ + "websiteUrl": URL } } * @@ -61,9 +72,12 @@ public class HeatingElement extends AbstractOpenemsApp implements Open public static enum Property implements DefaultEnum { ALIAS("Heating Element App"), // CTRL_IO_HEATING_ELEMENT_ID("ctrlIoHeatingElement0"), // + OUTPUT_CHANNEL_PHASE_L1("io0/Relay1"), // + OUTPUT_CHANNEL_PHASE_L2("io0/Relay2"), // + OUTPUT_CHANNEL_PHASE_L3("io0/Relay3"), // ; - private String defaultValue; + private final String defaultValue; private Property(String defaultValue) { this.defaultValue = defaultValue; @@ -83,38 +97,83 @@ public HeatingElement(@Reference ComponentManager componentManager, ComponentCon } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { final var heatingElementId = this.getId(t, p, Property.CTRL_IO_HEATING_ELEMENT_ID); - final var alias = this.getValueOrDefault(p, Property.ALIAS); + final var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); + final var outputChannelPhaseL1 = this.getValueOrDefault(p, Property.OUTPUT_CHANNEL_PHASE_L1); + final var outputChannelPhaseL2 = this.getValueOrDefault(p, Property.OUTPUT_CHANNEL_PHASE_L2); + final var outputChannelPhaseL3 = this.getValueOrDefault(p, Property.OUTPUT_CHANNEL_PHASE_L3); List comp = new ArrayList<>(); var jsonConfigBuilder = JsonUtils.buildJsonObject(); - if (!t.isDeleteOrTest()) { - var relays = this.componentUtil.getPreferredRelays(Lists.newArrayList(heatingElementId), - new int[] { 1, 2, 3 }, new int[] { 4, 5, 6 }); - if (relays == null) { - throw new OpenemsException("Not enought relays available!"); - } - - jsonConfigBuilder.addProperty("outputChannelPhaseL1", relays[0]) // - .addProperty("outputChannelPhaseL2", relays[1]) // - .addProperty("outputChannelPhaseL3", relays[2]); // - } - comp.add(new EdgeConfig.Component(heatingElementId, alias, "Controller.IO.HeatingElement", - jsonConfigBuilder.build()));// + jsonConfigBuilder.addProperty("outputChannelPhaseL1", outputChannelPhaseL1) // + .addProperty("outputChannelPhaseL2", outputChannelPhaseL2) // + .addProperty("outputChannelPhaseL3", outputChannelPhaseL3) // + .build()));// + + var componentIdOfRelay = outputChannelPhaseL1.substring(0, outputChannelPhaseL1.indexOf('/')); + var appIdOfRelay = DependencyUtil.getInstanceIdOfAppWhichHasComponent(this.componentManager, + componentIdOfRelay, this.getAppId()); + + if (appIdOfRelay == null) { + // relay may be created but not as a app + return new AppConfiguration(comp); + } - return new AppConfiguration(comp); + var dependencies = Lists.newArrayList(new DependencyDeclaration("RELAY", // + DependencyDeclaration.CreatePolicy.NEVER, // + DependencyDeclaration.UpdatePolicy.NEVER, // + DependencyDeclaration.DeletePolicy.NEVER, // + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ALL, // + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED, // + DependencyDeclaration.AppDependencyConfig.create() // + .setSpecificInstanceId(appIdOfRelay) // + .build())); + + return new AppConfiguration(comp, null, null, dependencies); }; } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + var relays = this.componentUtil.getPreferredRelays(Lists.newArrayList(), new int[] { 1, 2, 3 }, + new int[] { 4, 5, 6 }); + var options = this.componentUtil.getAllRelays() // + .stream().map(r -> r.relays).flatMap(List::stream) // + .collect(Collectors.toList()); + return AppAssistant.create(this.getName(language)) // + .fields(JsonUtils.buildJsonArray() // + .add(JsonFormlyUtil.buildSelect(Property.OUTPUT_CHANNEL_PHASE_L1) // + .setOptions(options) // + .onlyIf(relays != null, t -> t.setDefaultValue(relays[0])) // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannelPhaseL1.label")) + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannelPhaseL1.description")) + .build()) + .add(JsonFormlyUtil.buildSelect(Property.OUTPUT_CHANNEL_PHASE_L2) // + .setOptions(options) // + .onlyIf(relays != null, t -> t.setDefaultValue(relays[1])) // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannelPhaseL2.label")) + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannelPhaseL2.description")) + .build()) + .add(JsonFormlyUtil.buildSelect(Property.OUTPUT_CHANNEL_PHASE_L3) // + .setOptions(options) // + .onlyIf(relays != null, t -> t.setDefaultValue(relays[2])) // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannelPhaseL3.label")) + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannelPhaseL3.description")) + .build()) + .build()) .build(); } @@ -130,24 +189,13 @@ public OpenemsAppCategory[] getCategorys() { } @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public Builder getValidateBuilder() { - return Validator.create() // - .setInstallableCheckableNames(new Validator.MapBuilder<>(new TreeMap>()) // - .put(CheckRelayCount.COMPONENT_NAME, // - new Validator.MapBuilder<>(new TreeMap()) // + public ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setInstallableCheckableConfigs(Lists.newArrayList(// + new ValidatorConfig.CheckableConfig(CheckRelayCount.COMPONENT_NAME, + new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("count", 3) // - .build()) - .build()); - } - - @Override - public String getName() { - return "Heizstab"; + .build()))); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome.java b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome.java index 9b7432f4c7d..fe0b79f05ec 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome.java +++ b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.EnumMap; import java.util.List; +import java.util.Optional; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -16,11 +17,15 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.EnumUtils; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.integratedsystem.FeneconHome.Property; +import io.openems.edge.app.meter.SocomecMeter; +import io.openems.edge.app.pvselfconsumption.GridOptimizedCharge; +import io.openems.edge.app.pvselfconsumption.SelfConsumptionOptimization; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.core.appmanager.AbstractOpenemsApp; import io.openems.edge.core.appmanager.AppAssistant; @@ -33,6 +38,8 @@ 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.dependency.DependencyDeclaration; /** * Describes a FENECON Home energy storage system. @@ -45,6 +52,7 @@ "image": base64, "properties":{ "SAFETY_COUNTRY":"AUSTRIA", + "RIPPLE_CONTROL_RECEIVER_ACTIV":false, "MAX_FEED_IN_POWER":5000, "FEED_IN_SETTING":"PU_ENABLE_CURVE", "HAS_AC_METER":true, @@ -56,7 +64,23 @@ "EMERGENCY_RESERVE_ENABLED":true, "EMERGENCY_RESERVE_SOC":20 }, - "appDescriptor": {} + "dependencies": [ + { + "key": "GRID_OPTIMIZED_CHARGE", + "instanceId": UUID + }, + { + "key": "AC_METER", + "instanceId": UUID + }, + { + "key": "SELF_CONSUMTION_OPTIMIZATION", + "instanceId": UUID + } + ], + "appDescriptor": { + "websiteUrl": URL + } } * */ @@ -69,6 +93,9 @@ public static enum Property { MAX_FEED_IN_POWER, // FEED_IN_SETTING, // + // (ger. Rundsteuerempfänger) + RIPPLE_CONTROL_RECEIVER_ACTIV, // + // External AC PV HAS_AC_METER, // @@ -96,26 +123,37 @@ public FeneconHome(@Reference ComponentManager componentManager, ComponentContex @Override public AppDescriptor getAppDescriptor() { return AppDescriptor.create() // + .setWebsiteUrl("https://fenecon.de/home/") // .build(); } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, // + AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { var essId = "ess0"; var modbusIdInternal = "modbus0"; var modbusIdExternal = "modbus1"; var emergencyReserveEnabled = EnumUtils.getAsBoolean(p, Property.EMERGENCY_RESERVE_ENABLED); + var rippleControlReceiverActive = EnumUtils.getAsOptionalBoolean(p, Property.RIPPLE_CONTROL_RECEIVER_ACTIV) + .orElse(false); // Battery-Inverter Settings var safetyCountry = EnumUtils.getAsString(p, Property.SAFETY_COUNTRY); - var maxFeedInPower = EnumUtils.getAsInt(p, Property.MAX_FEED_IN_POWER); + int maxFeedInPower; + if (!rippleControlReceiverActive) { + maxFeedInPower = EnumUtils.getAsInt(p, Property.MAX_FEED_IN_POWER); + } else { + maxFeedInPower = 0; + } var feedInSetting = EnumUtils.getAsString(p, Property.FEED_IN_SETTING); + var bundle = AbstractOpenemsApp.getTranslationBundle(l); var components = Lists.newArrayList(// - new EdgeConfig.Component(modbusIdInternal, "Kommunikation mit der Batterie", "Bridge.Modbus.Serial", - JsonUtils.buildJsonObject() // + new EdgeConfig.Component(modbusIdInternal, + TranslationUtil.getTranslation(bundle, this.getAppId() + "." + modbusIdInternal + ".alias"), + "Bridge.Modbus.Serial", JsonUtils.buildJsonObject() // .addProperty("enabled", true) // .addProperty("portName", "/dev/busUSB1") // .addProperty("baudRate", 19200) // @@ -123,9 +161,11 @@ protected ThrowingBiFunction .addProperty("stopbits", "ONE") // .addProperty("parity", "NONE") // .addProperty("logVerbosity", "NONE") // - .addProperty("invalidateElementsAfterReadErrors", 1) // - .build()), - new EdgeConfig.Component(modbusIdExternal, "Kommunikation mit dem Batterie-Wechselrichter", + .onlyIf(t == ConfigurationTarget.ADD, // + j -> j.addProperty("invalidateElementsAfterReadErrors", 1) // + ).build()), + new EdgeConfig.Component(modbusIdExternal, + TranslationUtil.getTranslation(bundle, this.getAppId() + "." + modbusIdExternal + ".alias"), "Bridge.Modbus.Serial", JsonUtils.buildJsonObject() // .addProperty("enabled", true) // .addProperty("portName", "/dev/busUSB2") // @@ -136,19 +176,24 @@ protected ThrowingBiFunction .addProperty("logVerbosity", "NONE") // .addProperty("invalidateElementsAfterReadErrors", 1) // .build()), - new EdgeConfig.Component("meter0", "Netzzähler", "GoodWe.Grid-Meter", // + new EdgeConfig.Component("meter0", + TranslationUtil.getTranslation(bundle, this.getAppId() + ".meter0.alias"), + "GoodWe.Grid-Meter", // JsonUtils.buildJsonObject() // .addProperty("enabled", true) // .addProperty("modbus.id", modbusIdExternal) // .addProperty("modbusUnitId", 247) // .build()), - new EdgeConfig.Component("io0", "Relaisboard", "IO.KMtronic.4Port", // + new EdgeConfig.Component("io0", + TranslationUtil.getTranslation(bundle, this.getAppId() + ".io0.alias"), "IO.KMtronic.4Port", // JsonUtils.buildJsonObject() // .addProperty("enabled", true) // .addProperty("modbus.id", modbusIdInternal) // .addProperty("modbusUnitId", 2) // .build()), - new EdgeConfig.Component("battery0", "Batterie", "Battery.Fenecon.Home", // + new EdgeConfig.Component("battery0", + TranslationUtil.getTranslation(bundle, this.getAppId() + ".battery0.alias"), + "Battery.Fenecon.Home", // JsonUtils.buildJsonObject() // .addProperty("enabled", true) // .addProperty("startStop", "AUTO") // @@ -156,64 +201,45 @@ protected ThrowingBiFunction .addProperty("modbusUnitId", 1) // .addProperty("batteryStartUpRelay", "io0/Relay4") // .build()), - new EdgeConfig.Component("batteryInverter0", "Batterie-Wechselrichter", "GoodWe.BatteryInverter", - JsonUtils.buildJsonObject() // + new EdgeConfig.Component("batteryInverter0", + TranslationUtil.getTranslation(bundle, this.getAppId() + ".batteryInverter0.alias"), + "GoodWe.BatteryInverter", JsonUtils.buildJsonObject() // .addProperty("enabled", true) // .addProperty("modbus.id", modbusIdExternal) // .addProperty("modbusUnitId", 247) // .addProperty("safetyCountry", safetyCountry) // - .addProperty("backupEnable", emergencyReserveEnabled ? "ENABLE" : "DISABLE") // - .addProperty("feedPowerEnable", "ENABLE") // + .addProperty("backupEnable", // + emergencyReserveEnabled ? "ENABLE" : "DISABLE") // + .addProperty("feedPowerEnable", rippleControlReceiverActive ? "DISABLE" : "ENABLE") // .addProperty("feedPowerPara", maxFeedInPower) // .addProperty("setfeedInPowerSettings", feedInSetting) // .build()), - new EdgeConfig.Component(essId, "Speichersystem", "Ess.Generic.ManagedSymmetric", - JsonUtils.buildJsonObject() // + new EdgeConfig.Component(essId, + TranslationUtil.getTranslation(bundle, this.getAppId() + "." + essId + ".alias"), + "Ess.Generic.ManagedSymmetric", JsonUtils.buildJsonObject() // .addProperty("enabled", true) // .addProperty("startStop", "START") // .addProperty("batteryInverter.id", "batteryInverter0") // .addProperty("battery.id", "battery0") // .build()), - new EdgeConfig.Component("predictor0", "Prognose", "Predictor.PersistenceModel", - JsonUtils.buildJsonObject() // + new EdgeConfig.Component("predictor0", + TranslationUtil.getTranslation(bundle, this.getAppId() + ".predictor0.alias"), + "Predictor.PersistenceModel", JsonUtils.buildJsonObject() // .addProperty("enabled", true) // .add("channelAddresses", JsonUtils.buildJsonArray() // .add("_sum/ProductionActivePower") // .add("_sum/ConsumptionActivePower") // .build()) // .build()), - new EdgeConfig.Component("ctrlGridOptimizedCharge0", "Netzdienliche Beladung", - "Controller.Ess.GridOptimizedCharge", JsonUtils.buildJsonObject() // - .addProperty("enabled", true) // - .addProperty("ess.id", essId) // - .addProperty("meter.id", "meter0") // - .addProperty("sellToGridLimitEnabled", true) // - .addProperty("maximumSellToGridPower", maxFeedInPower) // - .build()), - new EdgeConfig.Component("ctrlEssSurplusFeedToGrid0", "Überschusseinspeisung", + new EdgeConfig.Component("ctrlEssSurplusFeedToGrid0", + TranslationUtil.getTranslation(bundle, + this.getAppId() + ".ctrlEssSurplusFeedToGrid0.alias"), "Controller.Ess.Hybrid.Surplus-Feed-To-Grid", JsonUtils.buildJsonObject() // .addProperty("enabled", true) // .addProperty("ess.id", essId) // - .build()), - new EdgeConfig.Component("ctrlBalancing0", "Eigenverbrauchsoptimierung", - "Controller.Symmetric.Balancing", JsonUtils.buildJsonObject() // - .addProperty("enabled", true) // - .addProperty("ess.id", essId) // - .addProperty("meter.id", "meter0") // - .addProperty("targetGridSetpoint", 0) // - .build()) - + .build()) // ); - if (EnumUtils.getAsOptionalBoolean(p, Property.HAS_AC_METER).orElse(false)) { - components.add(new EdgeConfig.Component("meter1", "Netzzähler", "Meter.Socomec.Threephase", // - JsonUtils.buildJsonObject() // - .addProperty("enabled", true) // - .addProperty("modbus.id", modbusIdExternal) // - .addProperty("modbusUnitId", 6) // - .build())); - } - if (EnumUtils.getAsOptionalBoolean(p, Property.HAS_DC_PV1).orElse(false)) { var alias = EnumUtils.getAsOptionalString(p, Property.DC_PV1_ALIAS).orElse("DC-PV 1"); components.add(new EdgeConfig.Component("charger0", alias, "GoodWe.Charger-PV1", // @@ -238,7 +264,9 @@ protected ThrowingBiFunction var hasEmergencyReserve = EnumUtils.getAsOptionalBoolean(p, Property.HAS_EMERGENCY_RESERVE).orElse(false); if (hasEmergencyReserve) { - components.add(new EdgeConfig.Component("meter2", "Notstromverbraucher", "GoodWe.EmergencyPowerMeter", // + components.add(new EdgeConfig.Component("meter2", + TranslationUtil.getTranslation(bundle, this.getAppId() + ".meter2.alias"), + "GoodWe.EmergencyPowerMeter", // JsonUtils.buildJsonObject() // .addProperty("enabled", true) // .addProperty("modbus.id", modbusIdExternal) // @@ -247,7 +275,9 @@ protected ThrowingBiFunction var emergencyReserveSoc = EnumUtils.getAsInt(p, Property.EMERGENCY_RESERVE_SOC); components.add(new EdgeConfig.Component("ctrlEmergencyCapacityReserve0", - "Ansteuerung der Notstromreserve", "Controller.Ess.EmergencyCapacityReserve", // + TranslationUtil.getTranslation(bundle, + this.getAppId() + ".ctrlEmergencyCapacityReserve0.alias"), + "Controller.Ess.EmergencyCapacityReserve", // JsonUtils.buildJsonObject() // .addProperty("enabled", true) // .addProperty("ess.id", essId) // @@ -257,6 +287,20 @@ protected ThrowingBiFunction .build())); } + var hasAcMeter = EnumUtils.getAsOptionalBoolean(p, Property.HAS_AC_METER).orElse(false); + + // remove components that were in the old configuration but now are a dependency + if (t == ConfigurationTarget.DELETE) { + components.add(new EdgeConfig.Component("ctrlGridOptimizedCharge0", "", + "Controller.Ess.GridOptimizedCharge", JsonUtils.buildJsonObject().build())); + components.add(new EdgeConfig.Component("ctrlBalancing0", "", "Controller.Symmetric.Balancing", + JsonUtils.buildJsonObject().build())); + if (hasAcMeter) { + components.add(new EdgeConfig.Component("meter1", "", "Meter.Socomec.Threephase", + JsonUtils.buildJsonObject().build())); + } + } + /* * Set Execution Order for Scheduler. */ @@ -268,94 +312,205 @@ protected ThrowingBiFunction schedulerExecutionOrder.add("ctrlEssSurplusFeedToGrid0"); schedulerExecutionOrder.add("ctrlBalancing0"); - return new AppConfiguration(components, schedulerExecutionOrder); + var dependencies = Lists.newArrayList(new DependencyDeclaration("GRID_OPTIMIZED_CHARGE", // + DependencyDeclaration.CreatePolicy.IF_NOT_EXISTING, // + DependencyDeclaration.UpdatePolicy.ALWAYS, // + DependencyDeclaration.DeletePolicy.IF_MINE, // + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ONLY_UNCONFIGURED_PROPERTIES, // + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED, // + DependencyDeclaration.AppDependencyConfig.create() // + .setAppId("App.PvSelfConsumption.GridOptimizedCharge") // + .setProperties(JsonUtils.buildJsonObject() // + .addProperty(GridOptimizedCharge.Property.SELL_TO_GRID_LIMIT_ENABLED.name(), + !rippleControlReceiverActive) // + .addProperty(GridOptimizedCharge.Property.MAXIMUM_SELL_TO_GRID_POWER.name(), + maxFeedInPower) // + .build()) + .build()), + new DependencyDeclaration("SELF_CONSUMTION_OPTIMIZATION", // + DependencyDeclaration.CreatePolicy.IF_NOT_EXISTING, // + DependencyDeclaration.UpdatePolicy.NEVER, // + DependencyDeclaration.DeletePolicy.IF_MINE, // + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ONLY_UNCONFIGURED_PROPERTIES, // + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED, // + DependencyDeclaration.AppDependencyConfig.create() // + .setAppId("App.PvSelfConsumption.SelfConsumptionOptimization") // + .setProperties(JsonUtils.buildJsonObject() // + .addProperty(SelfConsumptionOptimization.Property.ESS_ID.name(), essId) // + .addProperty(SelfConsumptionOptimization.Property.METER_ID.name(), "meter0") // + .build()) + .build()) // + ); + + if (hasAcMeter) { + dependencies.add(new DependencyDeclaration("AC_METER", // + DependencyDeclaration.CreatePolicy.ALWAYS, // + DependencyDeclaration.UpdatePolicy.ALWAYS, // + DependencyDeclaration.DeletePolicy.IF_MINE, // + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ONLY_UNCONFIGURED_PROPERTIES, // + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED, // + DependencyDeclaration.AppDependencyConfig.create() // + .setAppId("App.Meter.Socomec") // + .setProperties(JsonUtils.buildJsonObject() // + .addProperty(SocomecMeter.Property.MODBUS_UNIT_ID.name(), 6) // + .build()) + .build())); + } + + return new AppConfiguration(components, schedulerExecutionOrder, null, dependencies); }; } @Override - public AppAssistant getAppAssistant() { - // Source https://formly.dev/examples/introduction - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + final var batteryInverter = this.getBatteryInverter(); + final var hasEmergencyReserve = this.componentUtil.getComponent("ctrlEmergencyCapacityReserve0", // + "Controller.Ess.EmergencyCapacityReserve").isPresent(); + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildSelect(Property.SAFETY_COUNTRY) // - .setLabel("Battery-Inverter Safety Country") // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".safetyCountry.label")) // .isRequired(true) // .setOptions(JsonUtils.buildJsonArray() // .add(JsonUtils.buildJsonObject() // - .addProperty("label", "Germany") // + .addProperty("label", // + TranslationUtil.getTranslation(bundle, "germany")) // .addProperty("value", "GERMANY") // .build()) // .add(JsonUtils.buildJsonObject() // - .addProperty("label", "Austria") // + .addProperty("label", // + TranslationUtil.getTranslation(bundle, "austria")) // .addProperty("value", "AUSTRIA") // .build()) // .add(JsonUtils.buildJsonObject() // - .addProperty("label", "Switzerland") // + .addProperty("label", // + TranslationUtil.getTranslation(bundle, "switzerland")) // .addProperty("value", "SWITZERLAND") // .build()) // .build()) // + .onlyIf(batteryInverter.isPresent(), f -> { + f.setDefaultValue(batteryInverter.get() // + .getProperty("safetyCountry").get().getAsString()); + }).build()) + .add(JsonFormlyUtil.buildCheckbox(Property.RIPPLE_CONTROL_RECEIVER_ACTIV) // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".rippleControlReceiver.label")) + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".rippleControlReceiver.description")) + .setDefaultValue(false) // .build()) .add(JsonFormlyUtil.buildInput(Property.MAX_FEED_IN_POWER) // - .setLabel("Feed-In limitation [W]") // + .setLabel( + TranslationUtil.getTranslation(bundle, this.getAppId() + ".feedInLimit.label")) // .isRequired(true) // + .onlyShowIfNotChecked(Property.RIPPLE_CONTROL_RECEIVER_ACTIV) // .setInputType(Type.NUMBER) // - .build()) + .onlyIf(batteryInverter.isPresent(), f -> { + f.setDefaultValue(batteryInverter.get() // + .getProperty("feedPowerPara").get()); + }).build()) .add(JsonFormlyUtil.buildSelect(Property.FEED_IN_SETTING) // - .setLabel("Feed-In Settings") // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".feedInSettings.label")) // .isRequired(true) // .setOptions(this.getFeedInSettingsOptions(), t -> t, t -> t) // - .build()) + .onlyIf(batteryInverter.isPresent(), f -> { + f.setDefaultValue(batteryInverter.get() // + .getProperty("setfeedInPowerSettings") // + .get().getAsString()); + }).build()) .add(JsonFormlyUtil.buildCheckbox(Property.HAS_AC_METER) // - .setLabel("Has AC meter (SOCOMEC)") // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".hasAcMeterSocomec.label")) // .isRequired(true) // + .setDefaultValue(this.componentUtil // + .getComponent("meter1", "Meter.Socomec.Threephase") // + .isPresent()) // .build()) .add(JsonFormlyUtil.buildCheckbox(Property.HAS_DC_PV1) // - .setLabel("Has DC-PV 1 (MPPT 1)") // + .setLabel(TranslationUtil.getTranslation(bundle, this.getAppId() + ".hasDcPV1.label")) // .isRequired(true) // + .setDefaultValue(this.componentUtil // + .getComponent("charger0", "GoodWe.Charger-PV1").isPresent()) .build()) .add(JsonFormlyUtil.buildInput(Property.DC_PV1_ALIAS) // - .setDefaultValue("DC-PV1") // .setLabel("DC-PV 1 Alias") // + .setDefaultValue("DC-PV1") // .onlyShowIfChecked(Property.HAS_DC_PV1) // + .onlyIf(this.componentUtil.getComponent("charger0", "GoodWe.Charger-PV1").isPresent(), + j -> j.setDefaultValueWithStringSupplier(() -> { + var charger = this.componentUtil // + .getComponent("charger0", "GoodWe.Charger-PV1"); + if (charger.isEmpty()) { + return null; + } + return charger.get().getAlias(); + })) .build()) .add(JsonFormlyUtil.buildCheckbox(Property.HAS_DC_PV2) // - .setLabel("Has DC-PV 2 (MPPT 2)") // + .setLabel(TranslationUtil.getTranslation(bundle, this.getAppId() + ".hasDcPV2.label")) // .isRequired(true) // + .setDefaultValue(this.componentUtil // + .getComponent("charger1", "GoodWe.Charger-PV2").isPresent()) .build()) .add(JsonFormlyUtil.buildInput(Property.DC_PV2_ALIAS) // - .setDefaultValue("DC-PV 2") // .setLabel("DC-PV 2 Alias") // + .setDefaultValue("DC-PV2") // .onlyShowIfChecked(Property.HAS_DC_PV2) // + .onlyIf(this.componentUtil.getComponent("charger1", "GoodWe.Charger-PV2").isPresent(), + j -> j.setDefaultValueWithStringSupplier(() -> { + var charger = this.componentUtil // + .getComponent("charger1", "GoodWe.Charger-PV2"); + if (charger.isEmpty()) { + return null; + } + return charger.get().getAlias(); + })) .build()) .add(JsonFormlyUtil.buildCheckbox(Property.EMERGENCY_RESERVE_ENABLED) // - .setLabel("Activate Emergency power supply") // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".emergencyPowerSupply.label")) // .isRequired(true) // - .build()) + .onlyIf(batteryInverter.isPresent(), f -> { + f.setDefaultValue(batteryInverter.get().getProperty("backupEnable").get() + .getAsString().equals("ENABLE")); + }).build()) .add(JsonFormlyUtil.buildCheckbox(Property.HAS_EMERGENCY_RESERVE) // - .setLabel("Activate Emergency Reserve Energy") // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".emergencyPowerEnergy.label")) // + .setDefaultValue(hasEmergencyReserve) // .onlyShowIfChecked(Property.EMERGENCY_RESERVE_ENABLED) // .build()) .add(JsonFormlyUtil.buildInput(Property.EMERGENCY_RESERVE_SOC) // - .setLabel("Emergency Reserve Energy (State-of-Charge)") // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".reserveEnergy.label")) // + .setInputType(Type.NUMBER) // + .setMin(0) // + .setMax(100) // .onlyShowIfChecked(Property.HAS_EMERGENCY_RESERVE) // - .build()) + .onlyIf(hasEmergencyReserve, f -> { + f.setDefaultValue(this.componentManager.getEdgeConfig() + .getComponent("ctrlEmergencyCapacityReserve0").get() + .getProperty("reserveSoc").get().getAsNumber()); + }).build()) .build()) // .build(); } - @Override - public OpenemsAppCategory[] getCategorys() { - return new OpenemsAppCategory[] { OpenemsAppCategory.INTEGRATED_SYSTEM }; - } - - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; + private final Optional getBatteryInverter() { + var batteryInverter = this.componentManager.getEdgeConfig().getComponent("batteryInverter0"); + if (batteryInverter.isPresent() // + && !batteryInverter.get().getFactoryId().equals("GoodWe.BatteryInverter")) { + batteryInverter = Optional.empty(); + } + return batteryInverter; } @Override - public String getName() { - return "FENECON Home"; + public OpenemsAppCategory[] getCategorys() { + return new OpenemsAppCategory[] { OpenemsAppCategory.INTEGRATED_SYSTEM }; } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ManualRelayControl.java b/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ManualRelayControl.java new file mode 100644 index 00000000000..ae048b0d887 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ManualRelayControl.java @@ -0,0 +1,164 @@ +package io.openems.edge.app.loadcontrol; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.TreeMap; +import java.util.stream.Collectors; + +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.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.session.Language; +import io.openems.common.types.EdgeConfig; +import io.openems.common.types.EdgeConfig.Component; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.loadcontrol.ManualRelayControl.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AppAssistant; +import io.openems.edge.core.appmanager.AppConfiguration; +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.DefaultEnum; +import io.openems.edge.core.appmanager.JsonFormlyUtil; +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.validator.CheckRelayCount; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; + +/** + * Describes a App for a manual relay control. + * + *
+  {
+    "appId":"App.LoadControl.ManualRelayControl",
+    "alias":"Manuelle Relaissteuerung",
+    "instanceId": UUID,
+    "image": base64,
+    "properties":{
+    	"CTRL_IO_FIX_DIGITAL_OUTPUT_ID": "ctrlIoFixDigitalOutput0",
+    	"OUTPUT_CHANNEL": "io1/Relay1"
+    },
+    "appDescriptor": {
+    	"websiteUrl": URL
+    }
+  }
+ * 
+ */ +@org.osgi.service.component.annotations.Component(name = "App.LoadControl.ManualRelayControl") +public class ManualRelayControl extends AbstractOpenemsApp implements OpenemsApp { + + public static enum Property implements DefaultEnum { + // User values + ALIAS("Manuelle Relaissteuerung"), // + OUTPUT_CHANNEL("io0/Relay1"), // + // Components + CTRL_IO_FIX_DIGITAL_OUTPUT_ID("ctrlIoFixDigitalOutput0"); + + private final String defaultValue; + + private Property(String defaultValue) { + this.defaultValue = defaultValue; + } + + @Override + public String getDefaultValue() { + return this.defaultValue; + } + + } + + @Activate + public ManualRelayControl(@Reference ComponentManager componentManager, ComponentContext componentContext, + @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { + + final var ctrlIoFixDigitalOutputId = this.getId(t, p, Property.CTRL_IO_FIX_DIGITAL_OUTPUT_ID); + + final var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); + + final var outputChannelAddress = this.getValueOrDefault(p, Property.OUTPUT_CHANNEL); + + List comp = new ArrayList<>(); + + comp.add(new EdgeConfig.Component(ctrlIoFixDigitalOutputId, alias, "Controller.Io.FixDigitalOutput", + JsonUtils.buildJsonObject() // + .addProperty("outputChannelAddress", outputChannelAddress) // + .build()));// + + return new AppConfiguration(comp); + }; + } + + @Override + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // + .fields(JsonUtils.buildJsonArray() // + .add(JsonFormlyUtil.buildSelect(Property.OUTPUT_CHANNEL) // + .setOptions(this.componentUtil.getAllRelays() // + .stream().map(r -> r.relays).flatMap(List::stream) // + .collect(Collectors.toList())) // + .setDefaultValueWithStringSupplier(() -> { + var relays = this.componentUtil.getPreferredRelays(Lists.newArrayList(), + new int[] { 1 }, new int[] { 1 }); + return relays == null ? null : relays[0]; + }) // + .isRequired(true) // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannel.label")) // + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannel.description")) // + .build()) + .build()) + .build(); + } + + @Override + public AppDescriptor getAppDescriptor() { + return AppDescriptor.create() // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategorys() { + return new OpenemsAppCategory[] { OpenemsAppCategory.LOAD_CONTROL }; + } + + @Override + public ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setInstallableCheckableConfigs(Lists.newArrayList(// + new ValidatorConfig.CheckableConfig(CheckRelayCount.COMPONENT_NAME, + new ValidatorConfig.MapBuilder<>(new TreeMap()) // + .put("count", 1) // + .build()))); + } + + @Override + protected Class getPropertyClass() { + return Property.class; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.MULTIPLE; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ThresholdControl.java b/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ThresholdControl.java new file mode 100644 index 00000000000..2007e9e4363 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ThresholdControl.java @@ -0,0 +1,169 @@ +package io.openems.edge.app.loadcontrol; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.TreeMap; +import java.util.stream.Collectors; + +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.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.session.Language; +import io.openems.common.types.EdgeConfig; +import io.openems.common.types.EdgeConfig.Component; +import io.openems.common.utils.EnumUtils; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.loadcontrol.ThresholdControl.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AppAssistant; +import io.openems.edge.core.appmanager.AppConfiguration; +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.DefaultEnum; +import io.openems.edge.core.appmanager.JsonFormlyUtil; +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.validator.CheckRelayCount; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; + +/** + * Describes a App for a Threshold Controller. + * + *
+  {
+    "appId":"App.LoadControl.ThresholdControl",
+    "alias":"Schwellwertsteuerung",
+    "instanceId": UUID,
+    "image": base64,
+    "properties":{
+    	"CTRL_IO_CHANNEL_SINGLE_THRESHOLD_ID": "ctrlIoChannelSingleThreshold0",
+    	"OUTPUT_CHANNELS":['io1/Relay1', 'io1/Relay2']
+    },
+    "appDescriptor": {
+    	"websiteUrl": URL
+    }
+  }
+ * 
+ */ +@org.osgi.service.component.annotations.Component(name = "App.LoadControl.ThresholdControl") +public class ThresholdControl extends AbstractOpenemsApp implements OpenemsApp { + + public static enum Property implements DefaultEnum { + // User values + ALIAS("Schwellwertsteuerung"), // + OUTPUT_CHANNELS("['io0/Relay1']"), // + // Components + CTRL_IO_CHANNEL_SINGLE_THRESHOLD_ID("ctrlIoChannelSingleThreshold0"); + + private final String defaultValue; + + private Property(String defaultValue) { + this.defaultValue = defaultValue; + } + + @Override + public String getDefaultValue() { + return this.defaultValue; + } + + } + + @Activate + public ThresholdControl(@Reference ComponentManager componentManager, ComponentContext componentContext, + @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { + + final var ctrlIoChannelSingleThresholdId = this.getId(t, p, Property.CTRL_IO_CHANNEL_SINGLE_THRESHOLD_ID); + + final var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); + + final var outputChannelAddress = EnumUtils.getAsJsonArray(p, Property.OUTPUT_CHANNELS); + + List comp = new ArrayList<>(); + + comp.add(new EdgeConfig.Component(ctrlIoChannelSingleThresholdId, alias, + "Controller.IO.ChannelSingleThreshold", JsonUtils.buildJsonObject() // + .onlyIf(t == ConfigurationTarget.ADD, + j -> j.addProperty("inputChannelAddress", "_sum/EssSoc")) + .add("outputChannelAddress", outputChannelAddress) // + .onlyIf(t == ConfigurationTarget.ADD, b -> b.addProperty("threshold", 50)) // + .build()));// + + return new AppConfiguration(comp); + }; + } + + @Override + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // + .fields(JsonUtils.buildJsonArray() // + .add(JsonFormlyUtil.buildSelect(Property.OUTPUT_CHANNELS) // + .isMulti(true) // + .setOptions(this.componentUtil.getAllRelays() // + .stream().map(r -> r.relays).flatMap(List::stream) // + .collect(Collectors.toList())) // + .setDefaultValueWithStringSupplier(() -> { + var relays = this.componentUtil.getPreferredRelays(Lists.newArrayList(), + new int[] { 1 }, new int[] { 1 }); + return relays == null ? null : relays[0]; + }) // + .isRequired(true) // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannels.label")) // + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".outputChannels.description")) // + .build()) + .build()) + .build(); + } + + @Override + public AppDescriptor getAppDescriptor() { + return AppDescriptor.create() // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategorys() { + return new OpenemsAppCategory[] { OpenemsAppCategory.LOAD_CONTROL }; + } + + @Override + public ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setInstallableCheckableConfigs(Lists.newArrayList(// + new ValidatorConfig.CheckableConfig(CheckRelayCount.COMPONENT_NAME, + new ValidatorConfig.MapBuilder<>(new TreeMap()) // + .put("count", 1) // + .build()))); + } + + @Override + protected Class getPropertyClass() { + return Property.class; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.MULTIPLE; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/AbstractMeterApp.java b/io.openems.edge.core/src/io/openems/edge/app/meter/AbstractMeterApp.java index 7d98beee953..dfb4b45b3a9 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/meter/AbstractMeterApp.java +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/AbstractMeterApp.java @@ -5,11 +5,13 @@ import com.google.gson.JsonArray; +import io.openems.common.session.Language; import io.openems.common.utils.JsonUtils; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.core.appmanager.AbstractOpenemsApp; import io.openems.edge.core.appmanager.ComponentUtil; import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.TranslationUtil; public abstract class AbstractMeterApp> extends AbstractOpenemsApp { @@ -23,18 +25,19 @@ public final OpenemsAppCategory[] getCategorys() { return new OpenemsAppCategory[] { OpenemsAppCategory.METER }; } - protected final JsonArray buildMeterOptions() { + protected final JsonArray buildMeterOptions(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); return JsonUtils.buildJsonArray() // .add(JsonUtils.buildJsonObject() // - .addProperty("label", "Erzeugung/Production") // + .addProperty("label", TranslationUtil.getTranslation(bundle, "App.Meter.production")) // .addProperty("value", "PRODUCTION") // .build()) .add(JsonUtils.buildJsonObject() // - .addProperty("label", "Netzzähler/Grid-Meter") // + .addProperty("label", TranslationUtil.getTranslation(bundle, "App.Meter.gridMeter")) // .addProperty("value", "GRID") // .build()) .add(JsonUtils.buildJsonObject() // - .addProperty("label", "Verbrauchszähler/Consumption-Meter") // + .addProperty("label", TranslationUtil.getTranslation(bundle, "App.Meter.consumtionMeter")) // .addProperty("value", "CONSUMPTION_METERED") // .build()) .build(); diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/CarloGavazziMeter.java b/io.openems.edge.core/src/io/openems/edge/app/meter/CarloGavazziMeter.java index 84560aace0d..d85512f9a9c 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/meter/CarloGavazziMeter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/CarloGavazziMeter.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.EnumMap; -import java.util.Map; import java.util.TreeMap; import org.osgi.service.cm.ConfigurationAdmin; @@ -11,15 +10,18 @@ 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.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.EnumUtils; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.meter.CarloGavazziMeter.Property; import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; import io.openems.edge.core.appmanager.AppAssistant; import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDescriptor; @@ -29,9 +31,9 @@ import io.openems.edge.core.appmanager.JsonFormlyUtil.InputBuilder.Type; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.TranslationUtil; import io.openems.edge.core.appmanager.validator.CheckHome; -import io.openems.edge.core.appmanager.validator.Validator; -import io.openems.edge.core.appmanager.validator.Validator.Builder; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** * Describes a app for a Carlo Gavazzi meter. @@ -48,8 +50,7 @@ "MODBUS_UNIT_ID": 6 }, "appDescriptor": { - "websiteUrl": https://fenecon.de/fems-2-2/fems-app-carlo-gavazzi-zaehler-2/ + "websiteUrl": URL } } * @@ -74,14 +75,14 @@ public CarloGavazziMeter(@Reference ComponentManager componentManager, Component } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { // modbus id for connection to battery-inverter for a HOME var modbusId = "modbus1"; var meterId = this.getId(t, p, Property.METER_ID, "meter1"); - var alias = this.getValueOrDefault(p, Property.ALIAS, "PV"); + var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); var type = this.getValueOrDefault(p, Property.TYPE, "PRODUCTION"); var modbusUnitId = EnumUtils.getAsInt(p, Property.MODBUS_UNIT_ID); @@ -100,16 +101,17 @@ protected ThrowingBiFunction } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildSelect(Property.TYPE) // - .setLabel("Mount Type") // - .setOptions(this.buildMeterOptions()) // + .setLabel(TranslationUtil.getTranslation(bundle, "App.Meter.mountType.label")) // + .setOptions(this.buildMeterOptions(language)) // .build()) // .add(JsonFormlyUtil.buildInput(Property.MODBUS_UNIT_ID) // - .setLabel("Modbus Unit-ID") // - .setDescription("The Unit-ID of the Modbus device.") // + .setLabel(TranslationUtil.getTranslation(bundle, "modbusUnitId")) // + .setDescription(TranslationUtil.getTranslation(bundle, "modbusUnitId.description")) // .setInputType(Type.NUMBER) // .setDefaultValue(6) // .setMin(0) // @@ -126,23 +128,12 @@ public AppDescriptor getAppDescriptor() { } @Override - public Builder getValidateBuilder() { - return Validator.create() // - .setCompatibleCheckableNames(new Validator.MapBuilder<>(new TreeMap>()) // - .put(CheckHome.COMPONENT_NAME, // - new Validator.MapBuilder<>(new TreeMap()) // - .build()) - .build()); - } - - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "Carlo Gavazzi Zähler"; + public ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setCompatibleCheckableConfigs(Lists.newArrayList(// + new ValidatorConfig.CheckableConfig(CheckHome.COMPONENT_NAME, + new ValidatorConfig.MapBuilder<>(new TreeMap()) // + .build()))); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/JanitzaMeter.java b/io.openems.edge.core/src/io/openems/edge/app/meter/JanitzaMeter.java index 9caa328844f..b0b395759cd 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/meter/JanitzaMeter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/JanitzaMeter.java @@ -14,15 +14,18 @@ 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.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.EnumUtils; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.meter.JanitzaMeter.Property; import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; import io.openems.edge.core.appmanager.AppAssistant; import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDescriptor; @@ -33,9 +36,9 @@ import io.openems.edge.core.appmanager.JsonFormlyUtil.InputBuilder.Validation; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.TranslationUtil; import io.openems.edge.core.appmanager.validator.CheckHome; -import io.openems.edge.core.appmanager.validator.Validator; -import io.openems.edge.core.appmanager.validator.Validator.Builder; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** * Describes a App for a Janitza meter. @@ -55,8 +58,7 @@ "MODBUS_UNIT_ID": 1 }, "appDescriptor": { - "websiteUrl": https://fenecon.de/fems-2-2/fems-app-janitza-zaehler-2/ + "websiteUrl": URL } } * @@ -84,8 +86,8 @@ public JanitzaMeter(@Reference ComponentManager componentManager, ComponentConte } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { var meterId = this.getId(t, p, Property.METER_ID, "meter1"); @@ -96,7 +98,7 @@ protected ThrowingBiFunction // var modbusId = "modbus1"; var modbusId = this.getId(t, p, Property.MODBUS_ID, "modbus2"); - var alias = this.getValueOrDefault(p, Property.ALIAS, "PV"); + var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); var factorieId = this.getValueOrDefault(p, Property.MODEL, "Meter.Janitza.UMG96RME"); var type = this.getValueOrDefault(p, Property.TYPE, "PRODUCTION"); var ip = this.getValueOrDefault(p, Property.IP, "10.4.0.12"); @@ -121,29 +123,30 @@ protected ThrowingBiFunction } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildSelect(Property.MODEL) // - .setLabel("Product Model") // + .setLabel(TranslationUtil.getTranslation(bundle, this.getAppId() + ".productModel")) // .isRequired(true) // .setOptions(this.buildFactorieIdOptions()) // .build()) // .add(JsonFormlyUtil.buildSelect(Property.TYPE) // - .setLabel("Mount Type") // + .setLabel(TranslationUtil.getTranslation(bundle, "App.Meter.mountType.label")) // .isRequired(true) // - .setOptions(this.buildMeterOptions()) // + .setOptions(this.buildMeterOptions(language)) // .build()) // .add(JsonFormlyUtil.buildInput(Property.IP) // - .setLabel("IP-Address") // - .setDescription("The IP address of the Meter.") // + .setLabel(TranslationUtil.getTranslation(bundle, "ipAddress")) // + .setDescription(TranslationUtil.getTranslation(bundle, "App.Meter.ip.description")) // .isRequired(true) // .setDefaultValue("10.4.0.12") // .setValidation(Validation.IP) // .build()) .add(JsonFormlyUtil.buildInput(Property.MODBUS_UNIT_ID) // - .setLabel("Modbus Unit-ID") // - .setDescription("The Unit-ID of the Modbus device.") // + .setLabel(TranslationUtil.getTranslation(bundle, "modbusUnitId")) // + .setDescription(TranslationUtil.getTranslation(bundle, "modbusUnitId.description")) // .setInputType(Type.NUMBER) // .setDefaultValue(1) // .setMin(0) // @@ -160,23 +163,12 @@ public AppDescriptor getAppDescriptor() { } @Override - public Builder getValidateBuilder() { - return Validator.create() // - .setCompatibleCheckableNames(new Validator.MapBuilder<>(new TreeMap>()) // - .put(CheckHome.COMPONENT_NAME, // - new Validator.MapBuilder<>(new TreeMap()) // - .build()) - .build()); - } - - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "Janitza Zähler"; + public ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setCompatibleCheckableConfigs(Lists.newArrayList(// + new ValidatorConfig.CheckableConfig(CheckHome.COMPONENT_NAME, + new ValidatorConfig.MapBuilder<>(new TreeMap()) // + .build()))); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/SocomecMeter.java b/io.openems.edge.core/src/io/openems/edge/app/meter/SocomecMeter.java index 8585953f85f..34faaf73e33 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/meter/SocomecMeter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/SocomecMeter.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.EnumMap; -import java.util.Map; import java.util.TreeMap; import org.osgi.service.cm.ConfigurationAdmin; @@ -11,15 +10,18 @@ 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.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.EnumUtils; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.meter.SocomecMeter.Property; import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; import io.openems.edge.core.appmanager.AppAssistant; import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDescriptor; @@ -29,9 +31,9 @@ import io.openems.edge.core.appmanager.JsonFormlyUtil.InputBuilder.Type; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.TranslationUtil; import io.openems.edge.core.appmanager.validator.CheckHome; -import io.openems.edge.core.appmanager.validator.Validator; -import io.openems.edge.core.appmanager.validator.Validator.Builder; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** * Describes a App for a Socomec meter. @@ -48,8 +50,7 @@ "MODBUS_UNIT_ID": 6 }, "appDescriptor": { - "websiteUrl": https://fenecon.de/fems/fems-app-socomec-zaehler-2 + "websiteUrl": URL } } * @@ -74,14 +75,14 @@ public SocomecMeter(@Reference ComponentManager componentManager, ComponentConte } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { // modbus id for connection to battery-inverter for a HOME var modbusId = "modbus1"; var meterId = this.getId(t, p, Property.METER_ID, "meter1"); - var alias = this.getValueOrDefault(p, Property.ALIAS, "PV"); + var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); var type = this.getValueOrDefault(p, Property.TYPE, "PRODUCTION"); var modbusUnitId = EnumUtils.getAsInt(p, Property.MODBUS_UNIT_ID); @@ -99,16 +100,18 @@ protected ThrowingBiFunction } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildSelect(Property.TYPE) // - .setLabel("Mount Type") // - .setOptions(this.buildMeterOptions()) // + .setLabel(TranslationUtil.getTranslation(bundle, "App.Meter.mountType.label")) // + .setOptions(this.buildMeterOptions(language)) // + .setDefaultValue("PRODUCTION") // .build()) // .add(JsonFormlyUtil.buildInput(Property.MODBUS_UNIT_ID) // - .setLabel("Modbus Unit-ID") // - .setDescription("The Unit-ID of the Modbus device.") // + .setLabel(TranslationUtil.getTranslation(bundle, "modbusUnitId")) // + .setDescription(TranslationUtil.getTranslation(bundle, "modbusUnitId.description")) // .setInputType(Type.NUMBER) // .setDefaultValue(6) // .setMin(0) // @@ -125,23 +128,12 @@ public AppDescriptor getAppDescriptor() { } @Override - public Builder getValidateBuilder() { - return Validator.create() // - .setCompatibleCheckableNames(new Validator.MapBuilder<>(new TreeMap>()) // - .put(CheckHome.COMPONENT_NAME, // - new Validator.MapBuilder<>(new TreeMap()) // - .build()) - .build()); - } - - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "Socomec Zähler"; + public ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setCompatibleCheckableConfigs(Lists.newArrayList(// + new ValidatorConfig.CheckableConfig(CheckHome.COMPONENT_NAME, + new ValidatorConfig.MapBuilder<>(new TreeMap()) // + .build()))); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/AbstractPvInverter.java b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/AbstractPvInverter.java index 35422002028..5fef4c4932e 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/AbstractPvInverter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/AbstractPvInverter.java @@ -13,7 +13,6 @@ import io.openems.edge.common.component.ComponentManager; import io.openems.edge.core.appmanager.AbstractOpenemsApp; import io.openems.edge.core.appmanager.ComponentUtil; -import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCategory; public abstract class AbstractPvInverter> extends AbstractOpenemsApp { @@ -43,13 +42,4 @@ protected final List getComponents(String factoryId, String pvInverte ); } - protected final Component getComponentWithFactoryId(List components, String factoryId) { - return components.stream().filter(t -> t.getFactoryId().equals(factoryId)).findFirst().orElse(null); - } - - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - } diff --git a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/KacoPvInverter.java b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/KacoPvInverter.java index 95a53f75a75..5ddb758061a 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/KacoPvInverter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/KacoPvInverter.java @@ -10,11 +10,13 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.utils.EnumUtils; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.pvinverter.KacoPvInverter.Property; import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; import io.openems.edge.core.appmanager.AppAssistant; import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDescriptor; @@ -25,6 +27,7 @@ import io.openems.edge.core.appmanager.JsonFormlyUtil.InputBuilder.Validation; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.TranslationUtil; /** * Describes a App for Kaco PV-Inverter. @@ -42,8 +45,7 @@ "PORT": "502" }, "appDescriptor": { - "websiteUrl": https://fenecon.de/fems-2-2/fems-app-kaco-pv-wechselrichter/ + "websiteUrl": URL } } * @@ -68,10 +70,10 @@ public KacoPvInverter(@Reference ComponentManager componentManager, ComponentCon } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { - var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName()); + var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); var ip = this.getValueOrDefault(p, Property.IP, "192.168.178.85"); var port = EnumUtils.getAsInt(p, Property.PORT); @@ -86,19 +88,21 @@ protected ThrowingBiFunction } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildInput(Property.IP) // - .setLabel("IP-Address") // - .setDescription("The IP address of the Pv-Inverter.") // + .setLabel(TranslationUtil.getTranslation(bundle, "ipAddress")) // + .setDescription(TranslationUtil.getTranslation(bundle, "App.PvInverter.ip.description")) // .setDefaultValue("192.168.178.85") // .isRequired(true) // .setValidation(Validation.IP) // .build()) // .add(JsonFormlyUtil.buildInput(Property.PORT) // - .setLabel("Port") // - .setDescription("The port of the Pv-Inverter.") // + .setLabel(TranslationUtil.getTranslation(bundle, "port")) // + .setDescription( + TranslationUtil.getTranslation(bundle, "App.PvInverter.port.description")) // .setInputType(Type.NUMBER) // .setDefaultValue(502) // .setMin(0) // @@ -114,16 +118,6 @@ public AppDescriptor getAppDescriptor() { .build(); } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "KACO PV-Wechselrichter"; - } - @Override protected Class getPropertyClass() { return Property.class; diff --git a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/KostalPvInverter.java b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/KostalPvInverter.java index 52cdcbd2acb..0657d764f64 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/KostalPvInverter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/KostalPvInverter.java @@ -10,11 +10,13 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.utils.EnumUtils; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.pvinverter.KostalPvInverter.Property; import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; import io.openems.edge.core.appmanager.AppAssistant; import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDescriptor; @@ -25,6 +27,7 @@ import io.openems.edge.core.appmanager.JsonFormlyUtil.InputBuilder.Validation; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.TranslationUtil; /** * Describes a App for Kostal PV-Inverter. @@ -43,8 +46,7 @@ "MODBUS_UNIT_ID": "71" }, "appDescriptor": { - "websiteUrl": https://fenecon.de/fems-2-2/fems-app-kostal-pv-wechselrichter/ + "websiteUrl": URL } } * @@ -71,10 +73,10 @@ public KostalPvInverter(@Reference ComponentManager componentManager, ComponentC } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { - var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName()); + var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); var ip = this.getValueOrDefault(p, Property.IP, "192.168.178.85"); var port = EnumUtils.getAsInt(p, Property.PORT); var modbusUnitId = EnumUtils.getAsInt(p, Property.MODBUS_UNIT_ID); @@ -84,7 +86,7 @@ protected ThrowingBiFunction var factoryIdInverter = "PV-Inverter.Kostal"; var components = this.getComponents(factoryIdInverter, pvInverterId, modbusId, alias, ip, port); - var inverter = this.getComponentWithFactoryId(components, factoryIdInverter); + var inverter = AbstractOpenemsApp.getComponentWithFactoryId(components, factoryIdInverter); inverter.getProperties().put("modbusUnitId", JsonUtils.parse(Integer.toString(modbusUnitId))); return new AppConfiguration(components); @@ -92,27 +94,29 @@ protected ThrowingBiFunction } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildInput(Property.IP) // - .setLabel("IP-Address") // - .setDescription("The IP address of the Pv-Inverter.") // + .setLabel(TranslationUtil.getTranslation(bundle, "ipAddress")) // + .setDescription(TranslationUtil.getTranslation(bundle, "App.PvInverter.ip.description")) // .setDefaultValue("192.168.178.85") // .isRequired(true) // .setValidation(Validation.IP) // .build()) // .add(JsonFormlyUtil.buildInput(Property.PORT) // - .setLabel("Port") // - .setDescription("The port of the Pv-Inverter.") // + .setLabel(TranslationUtil.getTranslation(bundle, "port")) // + .setDescription( + TranslationUtil.getTranslation(bundle, "App.PvInverter.port.description")) // .setInputType(Type.NUMBER) // .setDefaultValue(502) // .setMin(0) // .isRequired(true) // .build()) // .add(JsonFormlyUtil.buildInput(Property.MODBUS_UNIT_ID) // - .setLabel("Modbus Unit-ID") // - .setDescription("The Unit-ID of the Modbus device.") // + .setLabel(TranslationUtil.getTranslation(bundle, "modbusUnitId")) // + .setDescription(TranslationUtil.getTranslation(bundle, "modbusUnitId.description")) // .setInputType(Type.NUMBER) // .setDefaultValue(71) // .setMin(0) // @@ -128,16 +132,6 @@ public AppDescriptor getAppDescriptor() { .build(); } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "Kostal PV-Wechselrichter"; - } - @Override protected Class getPropertyClass() { return Property.class; diff --git a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SmaPvInverter.java b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SmaPvInverter.java index 6b35c9f7178..32ad2026428 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SmaPvInverter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SmaPvInverter.java @@ -10,11 +10,13 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.utils.EnumUtils; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.pvinverter.SmaPvInverter.Property; import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; import io.openems.edge.core.appmanager.AppAssistant; import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDescriptor; @@ -25,6 +27,7 @@ import io.openems.edge.core.appmanager.JsonFormlyUtil.InputBuilder.Validation; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.TranslationUtil; /** * Describes a App for SMA PV-Inverter. @@ -43,8 +46,7 @@ "MODBUS_UNIT_ID": "126" }, "appDescriptor": { - "websiteUrl": https://fenecon.de/fems-2-2/fems-app-sma-pv-wechselrichter/ + "websiteUrl": URL } } * @@ -70,10 +72,10 @@ public SmaPvInverter(@Reference ComponentManager componentManager, ComponentCont } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { - var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName()); + var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); var ip = this.getValueOrDefault(p, Property.IP, "192.168.178.85"); var port = EnumUtils.getAsInt(p, Property.PORT); var modbusUnitId = EnumUtils.getAsInt(p, Property.MODBUS_UNIT_ID); @@ -83,7 +85,7 @@ protected ThrowingBiFunction var factoryIdInverter = "PV-Inverter.SMA.SunnyTripower"; var components = this.getComponents(factoryIdInverter, pvInverterId, modbusId, alias, ip, port); - var inverter = this.getComponentWithFactoryId(components, factoryIdInverter); + var inverter = AbstractOpenemsApp.getComponentWithFactoryId(components, factoryIdInverter); inverter.getProperties().put("modbusUnitId", JsonUtils.parse(Integer.toString(modbusUnitId))); return new AppConfiguration(components); @@ -91,29 +93,29 @@ protected ThrowingBiFunction } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildInput(Property.IP) // - .setLabel("IP-Address") // - .setDescription("The IP address of the Pv-Inverter.") // + .setLabel(TranslationUtil.getTranslation(bundle, "ipAddress")) // + .setDescription(TranslationUtil.getTranslation(bundle, "App.PvInverter.ip.description")) // .setDefaultValue("192.168.178.85") // .isRequired(true) // .setValidation(Validation.IP) // .build()) // .add(JsonFormlyUtil.buildInput(Property.PORT) // - .setLabel("Port") // - .setDescription("The port of the Pv-Inverter.") // + .setLabel(TranslationUtil.getTranslation(bundle, "port")) // + .setDescription( + TranslationUtil.getTranslation(bundle, "App.PvInverter.port.description")) // .setInputType(Type.NUMBER) // .setDefaultValue(502) // .setMin(0) // .isRequired(true) // .build()) // .add(JsonFormlyUtil.buildInput(Property.MODBUS_UNIT_ID) // - .setLabel("Modbus Unit-ID") // - .setDescription("The Unit-ID of the Modbus device." - + "Be aware, that according to the manual you need to add '123' to the value that you configured " - + "in the SMA web interface.") // + .setLabel(TranslationUtil.getTranslation(bundle, "modbusUnitId")) // + .setDescription(TranslationUtil.getTranslation(bundle, "modbusUnitId.description")) // .setInputType(Type.NUMBER) // .setDefaultValue(126) // .setMin(0) // @@ -129,16 +131,6 @@ public AppDescriptor getAppDescriptor() { .build(); } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "SMA PV-Wechselrichter"; - } - @Override protected Class getPropertyClass() { return Property.class; diff --git a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SolarEdgePvInverter.java b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SolarEdgePvInverter.java index 93736ec94a9..34b5f1b5043 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SolarEdgePvInverter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SolarEdgePvInverter.java @@ -10,11 +10,13 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.utils.EnumUtils; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.pvinverter.SolarEdgePvInverter.Property; import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; import io.openems.edge.core.appmanager.AppAssistant; import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDescriptor; @@ -25,6 +27,7 @@ import io.openems.edge.core.appmanager.JsonFormlyUtil.InputBuilder.Validation; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.TranslationUtil; /** * Describes a App for SolarEdge PV-Inverter. @@ -42,8 +45,7 @@ "PORT": "502" }, "appDescriptor": { - "websiteUrl": https://fenecon.de/fems-2-2/fems-app-solaredge-pv-wechselrichter/ + "websiteUrl": URL } } * @@ -69,10 +71,10 @@ public SolarEdgePvInverter(@Reference ComponentManager componentManager, Compone } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { - var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName()); + var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); var ip = this.getValueOrDefault(p, Property.IP, "192.168.178.85"); var port = EnumUtils.getAsInt(p, Property.PORT); @@ -87,19 +89,21 @@ protected ThrowingBiFunction } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildInput(Property.IP) // - .setLabel("IP-Address") // - .setDescription("The IP address of the Pv-Inverter.") // + .setLabel(TranslationUtil.getTranslation(bundle, "ipAddress")) // + .setDescription(TranslationUtil.getTranslation(bundle, "App.PvInverter.ip.description")) // .setDefaultValue("192.168.178.85") // .isRequired(true) // .setValidation(Validation.IP) // .build()) // .add(JsonFormlyUtil.buildInput(Property.PORT) // - .setLabel("Port") // - .setDescription("The port of the Pv-Inverter.") // + .setLabel(TranslationUtil.getTranslation(bundle, "port")) // + .setDescription( + TranslationUtil.getTranslation(bundle, "App.PvInverter.port.description")) // .setInputType(Type.NUMBER) // .setDefaultValue(502) // .setMin(0) // @@ -115,16 +119,6 @@ public AppDescriptor getAppDescriptor() { .build(); } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "SolarEdge PV-Wechselrichter"; - } - @Override protected Class getPropertyClass() { return Property.class; diff --git a/io.openems.edge.core/src/io/openems/edge/app/pvselfconsumption/GridOptimizedCharge.java b/io.openems.edge.core/src/io/openems/edge/app/pvselfconsumption/GridOptimizedCharge.java new file mode 100644 index 00000000000..c2a9fd01328 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/pvselfconsumption/GridOptimizedCharge.java @@ -0,0 +1,157 @@ +package io.openems.edge.app.pvselfconsumption; + +import java.util.EnumMap; +import java.util.List; + +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.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.session.Language; +import io.openems.common.types.EdgeConfig; +import io.openems.common.types.EdgeConfig.Component; +import io.openems.common.utils.EnumUtils; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.pvselfconsumption.GridOptimizedCharge.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AppAssistant; +import io.openems.edge.core.appmanager.AppConfiguration; +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.JsonFormlyUtil; +import io.openems.edge.core.appmanager.JsonFormlyUtil.InputBuilder.Type; +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; + +/** + * Describes a App for a Grid Optimized Charge. + * + *
+  {
+    "appId":"App.PvSelfConsumption.GridOptimizedCharge",
+    "alias":"Netzdienliche Beladung",
+    "instanceId": UUID,
+    "image": base64,
+    "properties":{
+    	"SELL_TO_GRID_LIMIT_ENABLED": true,
+    	"CTRL_GRID_OPTIMIZED_CHARGE_ID": "ctrlGridOptimizedCharge0",
+    	"MAXIMUM_SELL_TO_GRID_POWER": 10000
+    },
+    "appDescriptor": {
+    	"websiteUrl": URL
+    }
+  }
+ * 
+ */ +@org.osgi.service.component.annotations.Component(name = "App.PvSelfConsumption.GridOptimizedCharge") +public class GridOptimizedCharge extends AbstractOpenemsApp implements OpenemsApp { + + public static enum Property { + // User values + ALIAS, // + SELL_TO_GRID_LIMIT_ENABLED, // + MAXIMUM_SELL_TO_GRID_POWER, // + // Components + CTRL_GRID_OPTIMIZED_CHARGE_ID; + + } + + @Activate + public GridOptimizedCharge(@Reference ComponentManager componentManager, ComponentContext componentContext, + @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { + + final var ctrlIoFixDigitalOutputId = this.getId(t, p, Property.CTRL_GRID_OPTIMIZED_CHARGE_ID, + "ctrlGridOptimizedCharge0"); + + final var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); + + final var sellToGridLimitEnabled = EnumUtils.getAsOptionalBoolean(p, Property.SELL_TO_GRID_LIMIT_ENABLED) + .orElse(true); + + final int maximumSellToGridPower; + if (sellToGridLimitEnabled) { + maximumSellToGridPower = EnumUtils.getAsInt(p, Property.MAXIMUM_SELL_TO_GRID_POWER); + } else { + maximumSellToGridPower = 0; + } + + List comp = Lists.newArrayList(new EdgeConfig.Component(ctrlIoFixDigitalOutputId, alias, + "Controller.Ess.GridOptimizedCharge", JsonUtils.buildJsonObject() // + .addProperty("enabled", true) // + .onlyIf(t == ConfigurationTarget.ADD, // + j -> j.addProperty("ess.id", "ess0") // + .addProperty("meter.id", "meter0")) + .addProperty("sellToGridLimitEnabled", sellToGridLimitEnabled) // + .onlyIf(sellToGridLimitEnabled, + o -> o.addProperty("maximumSellToGridPower", maximumSellToGridPower)) // + .build()));// + + var schedulerExecutionOrder = Lists.newArrayList("ctrlGridOptimizedCharge0", "ctrlEssSurplusFeedToGrid0"); + + return new AppConfiguration(comp, schedulerExecutionOrder); + }; + } + + @Override + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // + .fields(JsonUtils.buildJsonArray() // + .add(JsonFormlyUtil.buildCheckbox(Property.SELL_TO_GRID_LIMIT_ENABLED) // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".sellToGridLimitEnabled.label")) // + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".sellToGridLimitEnabled.description")) // + .build()) + .add(JsonFormlyUtil.buildInput(Property.MAXIMUM_SELL_TO_GRID_POWER) // + .setInputType(Type.NUMBER) // + .isRequired(true) // + .setMin(0) // + .onlyShowIfChecked(Property.SELL_TO_GRID_LIMIT_ENABLED) // + .setLabel(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".maximumSellToGridPower.label")) // + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".maximumSellToGridPower.description")) // + .build()) + .build()) + .build(); + } + + @Override + public AppDescriptor getAppDescriptor() { + return AppDescriptor.create() // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategorys() { + return new OpenemsAppCategory[] { OpenemsAppCategory.PV_SELF_CONSUMPTION }; + } + + @Override + protected Class getPropertyClass() { + return Property.class; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/pvselfconsumption/SelfConsumptionOptimization.java b/io.openems.edge.core/src/io/openems/edge/app/pvselfconsumption/SelfConsumptionOptimization.java new file mode 100644 index 00000000000..27fa8c84da1 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/pvselfconsumption/SelfConsumptionOptimization.java @@ -0,0 +1,142 @@ +package io.openems.edge.app.pvselfconsumption; + +import java.util.EnumMap; +import java.util.List; + +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.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.session.Language; +import io.openems.common.types.EdgeConfig; +import io.openems.common.types.EdgeConfig.Component; +import io.openems.common.utils.EnumUtils; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.pvselfconsumption.SelfConsumptionOptimization.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AppAssistant; +import io.openems.edge.core.appmanager.AppConfiguration; +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.JsonFormlyUtil; +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.ess.api.ManagedSymmetricEss; +import io.openems.edge.meter.api.SymmetricMeter; + +/** + * Describes a App for a Grid Optimized Charge. + * + *
+  {
+    "appId":"App.PvSelfConsumption.SelfConsumptionOptimization",
+    "alias":"Eigenverbrauchsoptimierung",
+    "instanceId": UUID,
+    "image": base64,
+    "properties":{
+    	"ESS_ID": "ess0",
+    	"METER_ID": "meter0"
+    },
+    "appDescriptor": {
+    	"websiteUrl": URL
+    }
+  }
+ * 
+ */ +@org.osgi.service.component.annotations.Component(name = "App.PvSelfConsumption.SelfConsumptionOptimization") +public class SelfConsumptionOptimization extends AbstractOpenemsApp implements OpenemsApp { + + public static enum Property { + // User values + ALIAS, // + ESS_ID, // + METER_ID, // + ; + } + + @Activate + public SelfConsumptionOptimization(@Reference ComponentManager componentManager, ComponentContext componentContext, + @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { + + final var ctrlBalacingId = "ctrlBalancing0"; + + final var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); + final var essId = EnumUtils.getAsString(p, Property.ESS_ID); + final var meterId = EnumUtils.getAsString(p, Property.METER_ID); + + List comp = Lists.newArrayList(new EdgeConfig.Component(ctrlBalacingId, alias, + "Controller.Symmetric.Balancing", JsonUtils.buildJsonObject() // + .addProperty("enabled", true) // + .addProperty("ess.id", essId) // + .addProperty("meter.id", meterId) // + .addProperty("targetGridSetpoint", 0) // + .build()));// + + return new AppConfiguration(comp); + }; + } + + @Override + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // + .fields(JsonUtils.buildJsonArray() // + .add(JsonFormlyUtil.buildSelect(Property.ESS_ID)// + .setLabel(TranslationUtil.getTranslation(bundle, this.getAppId() + ".ess.label")) // + .setDescription( + TranslationUtil.getTranslation(bundle, this.getAppId() + ".ess.description")) // + .isRequired(true) // + .setOptions(this.componentManager.getEnabledComponentsOfType(ManagedSymmetricEss.class), + c -> c.id() + ": " + c.alias(), ManagedSymmetricEss::id) // + .build()) + .add(JsonFormlyUtil.buildSelect(Property.METER_ID)// + .setLabel(TranslationUtil.getTranslation(bundle, this.getAppId() + ".meter.label")) // + .setDescription( + TranslationUtil.getTranslation(bundle, this.getAppId() + ".meter.description")) // + .isRequired(true) // + .setOptions(this.componentManager.getEnabledComponentsOfType(SymmetricMeter.class), + c -> c.id() + ": " + c.alias(), SymmetricMeter::id) // + .build()) + .build()) + .build(); + } + + @Override + public AppDescriptor getAppDescriptor() { + return AppDescriptor.create() // + .setWebsiteUrl("https://fenecon.de/fems-2-2/fems-app-eigenverbrauchsoptimierung-2/") // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategorys() { + return new OpenemsAppCategory[] { OpenemsAppCategory.PV_SELF_CONSUMPTION }; + } + + @Override + protected Class getPropertyClass() { + return Property.class; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java index a300a9c9b94..51b28d70ef0 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java @@ -12,7 +12,8 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.types.EdgeConfig.Component; import io.openems.common.utils.JsonUtils; @@ -42,6 +43,7 @@ "TIME_OF_USE_TARIF_ID": "timeOfUseTariff0" }, "appDescriptor": { + "websiteUrl": URL } } * @@ -62,8 +64,8 @@ public AwattarHourly(@Reference ComponentManager componentManager, ComponentCont } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { var ctrlEssTimeOfUseTariffDischargeId = this.getId(t, p, Property.CTRL_ESS_TIME_OF_USE_TARIF_DISCHARGE_ID, "ctrlEssTimeOfUseTariffDischarge0"); @@ -72,11 +74,11 @@ protected ThrowingBiFunction // TODO ess id may be changed List comp = Lists.newArrayList(// - new EdgeConfig.Component(ctrlEssTimeOfUseTariffDischargeId, "aWATTar", + new EdgeConfig.Component(ctrlEssTimeOfUseTariffDischargeId, this.getName(l), "Controller.Ess.Time-Of-Use-Tariff.Discharge", JsonUtils.buildJsonObject() // .addProperty("ess.id", "ess0") // .build()), // - new EdgeConfig.Component(timeOfUseTariffId, "timeOfUseTariff0", "TimeOfUseTariff.Awattar", + new EdgeConfig.Component(timeOfUseTariffId, this.getName(l), "TimeOfUseTariff.Awattar", JsonUtils.buildJsonObject() // .build())// ); @@ -85,8 +87,8 @@ protected ThrowingBiFunction } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + return AppAssistant.create(this.getName(language)) // .build(); } @@ -101,16 +103,6 @@ public OpenemsAppCategory[] getCategorys() { return new OpenemsAppCategory[] { OpenemsAppCategory.TIME_OF_USE_TARIFF }; } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "Awattar HOURLY"; - } - @Override protected Class getPropertyClass() { return Property.class; diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java index 7d4d878830c..607de7a4d52 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java @@ -12,7 +12,8 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.types.EdgeConfig.Component; import io.openems.common.utils.EnumUtils; @@ -29,6 +30,7 @@ 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; /** * Describes a App for StromdaoCorrently. @@ -45,11 +47,12 @@ "ZIP_CODE": "12345678" }, "appDescriptor": { + "websiteUrl": URL } } * */ -@org.osgi.service.component.annotations.Component(name = "App.TimeVariablePrice.Stromdao") +@org.osgi.service.component.annotations.Component(name = "App.TimeOfUseTariff.Stromdao") public class StromdaoCorrently extends AbstractOpenemsApp implements OpenemsApp { public static enum Property { @@ -65,8 +68,8 @@ public StromdaoCorrently(@Reference ComponentManager componentManager, Component } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { var ctrlEssTimeOfUseTariffDischargeId = this.getId(t, p, Property.CTRL_ESS_TIME_OF_USE_TARIF_DISCHARGE_ID, "ctrlEssTimeOfUseTariffDischarge0"); @@ -76,11 +79,11 @@ protected ThrowingBiFunction // TODO ess id may be changed List comp = Lists.newArrayList(// - new EdgeConfig.Component(ctrlEssTimeOfUseTariffDischargeId, this.getName(), + new EdgeConfig.Component(ctrlEssTimeOfUseTariffDischargeId, this.getName(l), "Controller.Ess.Time-Of-Use-Tariff.Discharge", JsonUtils.buildJsonObject() // .addProperty("ess.id", "ess0") // .build()), // - new EdgeConfig.Component(timeOfUseTariffId, "timeOfUseTariff0", "TimeOfUseTariff.Corrently", + new EdgeConfig.Component(timeOfUseTariffId, this.getName(l), "TimeOfUseTariff.Corrently", JsonUtils.buildJsonObject() // .addProperty("zipcode", zipCode) // .build())// @@ -90,12 +93,14 @@ protected ThrowingBiFunction } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()) // + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)) // .fields(JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildInput(Property.ZIP_CODE) // - .setLabel("ZIP Code") // - .setDescription("German ZIP Code of the location.") // + .setLabel(TranslationUtil.getTranslation(bundle, this.getAppId() + ".zipCode.label")) // + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".zipCode.description")) // .isRequired(true) // .build()) // .build()) // @@ -113,16 +118,6 @@ public OpenemsAppCategory[] getCategorys() { return new OpenemsAppCategory[] { OpenemsAppCategory.TIME_OF_USE_TARIFF }; } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "Stromdao Corrently"; - } - @Override protected Class getPropertyClass() { return Property.class; diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java index ebb3a52f731..7c33e84c406 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java @@ -12,7 +12,8 @@ import com.google.gson.JsonElement; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.types.EdgeConfig.Component; import io.openems.common.utils.JsonUtils; @@ -29,6 +30,7 @@ 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; /** * Describes a App for Tibber. @@ -45,6 +47,7 @@ "ACCESS_TOKEN": {token} }, "appDescriptor": { + "websiteUrl": URL } } * @@ -66,8 +69,8 @@ public Tibber(@Reference ComponentManager componentManager, ComponentContext con } @Override - protected ThrowingBiFunction, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, p) -> { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, l) -> { var ctrlEssTimeOfUseTariffDischargeId = this.getId(t, p, Property.CTRL_ESS_TIME_OF_USE_TARIF_DISCHARGE_ID, "ctrlEssTimeOfUseTariffDischarge0"); @@ -77,11 +80,11 @@ protected ThrowingBiFunction // TODO ess id may be changed List comp = Lists.newArrayList(// - new EdgeConfig.Component(ctrlEssTimeOfUseTariffDischargeId, this.getName(), + new EdgeConfig.Component(ctrlEssTimeOfUseTariffDischargeId, this.getName(l), "Controller.Ess.Time-Of-Use-Tariff.Discharge", JsonUtils.buildJsonObject() // .addProperty("ess.id", "ess0") // .build()), // - new EdgeConfig.Component(timeOfUseTariffId, "timeOfUseTariff0", "TimeOfUseTariff.Tibber", + new EdgeConfig.Component(timeOfUseTariffId, this.getName(l), "TimeOfUseTariff.Tibber", JsonUtils.buildJsonObject() // .onlyIf(t.isAddOrUpdate(), c -> c.addProperty("accessToken", accessToken)) // .build())// @@ -95,12 +98,15 @@ protected ThrowingBiFunction } @Override - public AppAssistant getAppAssistant() { - return AppAssistant.create(this.getName()).fields(// + public AppAssistant getAppAssistant(Language language) { + var bundle = AbstractOpenemsApp.getTranslationBundle(language); + return AppAssistant.create(this.getName(language)).fields(// JsonUtils.buildJsonArray() // .add(JsonFormlyUtil.buildInput(Property.ACCESS_TOKEN) // - .setLabel("Access token") // - .setDescription("Access token for the Tibber API.") // + .setLabel( + TranslationUtil.getTranslation(bundle, this.getAppId() + ".accessToken.label")) // + .setDescription(TranslationUtil.getTranslation(bundle, + this.getAppId() + ".accessToken.description")) // .setInputType(Type.PASSWORD) // .isRequired(true) // .build()) // @@ -119,16 +125,6 @@ public OpenemsAppCategory[] getCategorys() { return new OpenemsAppCategory[] { OpenemsAppCategory.TIME_OF_USE_TARIFF }; } - @Override - public String getImage() { - return OpenemsApp.FALLBACK_IMAGE; - } - - @Override - public String getName() { - return "Tibber"; - } - @Override protected Class getPropertyClass() { return Property.class; 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 cfbd207bf2d..2b98a582b7e 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 @@ -6,6 +6,8 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.ResourceBundle; import java.util.TreeMap; import java.util.stream.Collectors; @@ -21,12 +23,17 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.function.ThrowingBiFunction; import io.openems.common.function.ThrowingFunction; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.types.EdgeConfig.Component; +import io.openems.common.utils.JsonUtils; import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.dependency.Dependency; +import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; import io.openems.edge.core.appmanager.validator.CheckCardinality; import io.openems.edge.core.appmanager.validator.Checkable; -import io.openems.edge.core.appmanager.validator.Validator; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; import io.openems.edge.core.host.NetworkInterface.Inet4AddressWithNetmask; public abstract class AbstractOpenemsApp> implements OpenemsApp { @@ -51,9 +58,10 @@ protected AbstractOpenemsApp(ComponentManager componentManager, ComponentContext * from a {@link EnumMap} of configuration properties for a given * {@link ConfigurationTarget}. */ - protected abstract ThrowingBiFunction, // configuration properties + Language, // the language AppConfiguration, // return value of the function OpenemsNamedException> // Exception on error appConfigurationFactory(); @@ -65,7 +73,7 @@ protected final void assertCheckables(ConfigurationTarget t, Checkable... checka final List errors = new ArrayList<>(); for (Checkable checkable : checkables) { if (!checkable.check()) { - errors.add(checkable.getErrorMessage()); + errors.add(checkable.getErrorMessage(Language.DEFAULT)); } } if (!errors.isEmpty()) { @@ -78,13 +86,14 @@ protected final void assertCheckables(ConfigurationTarget t, Checkable... checka * * @param errors a collection of validation errors * @param configurationTarget the target of the configuration + * @param language the language of the configuration * @param properties the configured App properties * @return the {@link AppConfiguration} or null */ private AppConfiguration configuration(ArrayList errors, ConfigurationTarget configurationTarget, - EnumMap properties) { + Language language, EnumMap properties) { try { - return this.appConfigurationFactory().apply(configurationTarget, properties); + return this.appConfigurationFactory().apply(configurationTarget, properties, language); } catch (OpenemsNamedException e) { errors.add(e.getMessage()); return null; @@ -124,11 +133,11 @@ private EnumMap convertToEnumMap(ArrayList errors } @Override - public AppConfiguration getAppConfiguration(ConfigurationTarget target, JsonObject config) + public AppConfiguration getAppConfiguration(ConfigurationTarget target, JsonObject config, Language language) throws OpenemsNamedException { var errors = new ArrayList(); var enumMap = this.convertToEnumMap(target != ConfigurationTarget.TEST ? errors : new ArrayList<>(), config); - var c = this.configuration(errors, target, enumMap); + var c = this.configuration(errors, target, language, enumMap); // TODO remove and maybe add @AttributeDefinition above enums // this is for removing passwords so they do not get saved @@ -214,13 +223,14 @@ protected String getId(ConfigurationTarget t, EnumMap map * Validate the App configuration. * * @param jProperties a JsonObject holding the App properties + * @param dependecies the dependencies of the current instance * @return a list of validation errors. Empty list says 'no errors' */ - private List getValidationErrors(JsonObject jProperties) { + private List getValidationErrors(JsonObject jProperties, List dependecies) { final var errors = new ArrayList(); final var properties = this.convertToEnumMap(errors, jProperties); - final var appConfiguration = this.configuration(errors, ConfigurationTarget.VALIDATE, properties); + final var appConfiguration = this.configuration(errors, ConfigurationTarget.VALIDATE, null, properties); if (appConfiguration == null) { return errors; } @@ -230,6 +240,14 @@ private List getValidationErrors(JsonObject jProperties) { this.validateComponentConfigurations(errors, edgeConfig, appConfiguration); this.validateScheduler(errors, edgeConfig, appConfiguration); + try { + var appManager = (AppManagerImpl) this.componentManager.getComponent(AppManager.SINGLETON_COMPONENT_ID); + this.validateDependecies(errors, dependecies, appConfiguration.dependencies, appManager); + } catch (OpenemsNamedException e) { + // AppManager not found + errors.add("No AppManager reachable!"); + } + // TODO remove 'if' if it works on windows // changing network settings only works on linux if (!System.getProperty("os.name").startsWith("Windows")) { @@ -240,25 +258,21 @@ private List getValidationErrors(JsonObject jProperties) { } @Override - public final Validator getValidator() { + public final ValidatorConfig getValidatorConfig() { Map properties = new TreeMap<>(); properties.put("openemsApp", this); // add check for cardinality for every app var validator = this.getValidateBuilder().build(); - validator.getInstallableCheckableNames().put(CheckCardinality.COMPONENT_NAME, properties); - if (this.installationValidation() != null) { - validator.setConfigurationValidation((t, u) -> { - var p = this.convertToEnumMap(new ArrayList<>(), u); - return this.installationValidation().apply(t, p); - }); - } + validator.getInstallableCheckableConfigs() + .add(new ValidatorConfig.CheckableConfig(CheckCardinality.COMPONENT_NAME, properties)); + return validator; } - protected Validator.Builder getValidateBuilder() { - return Validator.create(); + protected ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create(); } /** @@ -286,7 +300,7 @@ protected String getValueOrDefault(EnumMap map, DefaultEn protected String getValueOrDefault(EnumMap map, PROPERTY property, String defaultValue) { var element = map.get(property); if (element != null) { - return element.getAsString(); + return JsonUtils.getAsOptionalString(element).orElse(defaultValue); } return defaultValue; } @@ -329,7 +343,7 @@ public boolean hasProperty(String property) { @Override public void validate(OpenemsAppInstance instance) throws OpenemsNamedException { - var errors = this.getValidationErrors(instance.properties); + var errors = this.getValidationErrors(instance.properties, instance.dependencies); if (!errors.isEmpty()) { var error = errors.stream().collect(Collectors.joining("|")); throw new OpenemsException(error); @@ -358,7 +372,8 @@ private void validateComponentConfigurations(ArrayList errors, EdgeConfi missingComponents.add(componentId); continue; } - // ALIAS is not really necessary to validate + // ALIAS should not be validated because it can be different depending on the + // language ComponentUtilImpl.isSameConfigurationWithoutAlias(errors, expectedComponent, actualComponent); } @@ -388,7 +403,18 @@ private void validateIps(ArrayList errors, EdgeConfig actualEdgeConfig, var interfaces = this.componentUtil.getInterfaces(); var eth0 = interfaces.stream().filter(t -> t.getName().equals("eth0")).findFirst().get(); var eth0Adresses = eth0.getAddresses(); - addresses.removeAll(eth0Adresses.getValue()); + + var availableAddresses = new LinkedList(); + for (var address : addresses) { + for (var eth0Address : eth0Adresses.getValue()) { + if (eth0Address.isInSameNetwork(address)) { + availableAddresses.add(address); + break; + } + } + } + + addresses.removeAll(availableAddresses); for (var address : addresses) { errors.add("Address '" + address + "' is not added."); } @@ -432,4 +458,141 @@ private void validateScheduler(ArrayList errors, EdgeConfig actualEdgeCo errors.add("Controller [" + nextControllerId + "] is not/wrongly configured in Scheduler"); } } + + private void validateDependecies(List errors, List configDependencies, + List neededDependencies, AppManagerImpl appManager) { + + // find dependencies that are not in config + var notRegisteredDependencies = neededDependencies.stream().filter( + t -> configDependencies == null || !configDependencies.stream().anyMatch(o -> o.key.equals(t.key))) + .collect(Collectors.toList()); + + // check if exactly one app is available of the needed appId + for (var dependency : notRegisteredDependencies) { + List minErrors = null; + for (var appConfig : dependency.appConfigs) { + var appConfigErrors = new LinkedList(); + if (appConfig.specificInstanceId != null) { + try { + var instance = appManager.findInstanceById(appConfig.specificInstanceId); + checkProperties(errors, instance.properties, appConfig, dependency.key); + } catch (NoSuchElementException e) { + appConfigErrors.add("Specific InstanceId[" + appConfig.specificInstanceId + "] not found!"); + } + } else { + + var list = appManager.getInstantiatedApps().stream().filter(t -> t.appId.equals(appConfig.appId)) + .collect(Collectors.toList()); + if (list.size() != 1) { + errors.add("Missing dependency with Key[" + dependency.key + "] needed App[" + appConfig.appId + + "]"); + } else { + checkProperties(errors, list.get(0).properties, appConfig, dependency.key); + } + } + + if (minErrors == null || minErrors.size() > appConfigErrors.size()) { + minErrors = appConfigErrors; + } + } + + errors.addAll(minErrors); + } + + if (configDependencies == null) { + return; + } + // check if dependency apps are available + for (var dependency : configDependencies) { + try { + var appInstance = appManager.findInstanceById(dependency.instanceId); + var dd = neededDependencies.stream().filter(d -> d.key.equals(dependency.key)).findAny(); + if (dd.isEmpty()) { + errors.add("Can not get DependencyDeclaration of Dependency[" + dependency.key + "]"); + continue; + } + + // get app config + var appConfig = dd.get().appConfigs.stream() // + .filter(c -> c.specificInstanceId != null) // + .filter(c -> c.specificInstanceId.equals(appInstance.instanceId)).findAny(); + + if (appConfig.isEmpty()) { + appConfig = dd.get().appConfigs.stream() // + .filter(c -> c.appId != null) // + .filter(c -> c.appId.equals(appInstance.appId)).findAny(); + + if (appConfig.isEmpty()) { + errors.add("Can not get DependencyAppConfig of Dependency[" + dependency.key + "]"); + continue; + } + } + + // when available check properties + checkProperties(errors, appInstance.properties, appConfig.get(), dependency.key); + } catch (NoSuchElementException e) { + errors.add("App with instance[" + dependency.instanceId + "] not available!"); + } + } + } + + private static final void checkProperties(List errors, JsonObject actualAppProperties, + DependencyDeclaration.AppDependencyConfig appDependencyConfig, String dependecyKey) { + if (appDependencyConfig == null) { + errors.add("SubApp with Key[" + dependecyKey + "] not found!"); + return; + } + + for (var property : appDependencyConfig.properties.entrySet()) { + var actualValue = actualAppProperties.get(property.getKey()); + if (actualValue == null) { + errors.add("Value for Key[" + property.getKey() + "] not found!"); + continue; + } + var actual = actualValue.toString().replace("\"", ""); + var needed = property.getValue().toString().replace("\"", ""); + if (!actual.equals(needed)) { + errors.add("Value for Key[" + property.getKey() + "] does not match: expected[" + needed + "] actual[" + + actual + "] !"); + } + } + } + + @Override + public String getName(Language language) { + return AbstractOpenemsApp.getTranslation(language, this.getAppId() + ".Name"); + } + + @Override + public String getImage() { + return OpenemsApp.FALLBACK_IMAGE; + } + + protected static String getTranslation(Language language, String key) { + return TranslationUtil.getTranslation(getTranslationBundle(language), key); + } + + protected static ResourceBundle getTranslationBundle(Language language) { + if (language == null) { + language = Language.DEFAULT; + } + // TODO add language support + switch (language) { + case CZ: + case ES: + case FR: + case NL: + language = Language.EN; + break; + case DE: + case EN: + break; + } + return ResourceBundle.getBundle("io.openems.edge.core.appmanager.translation", language.getLocal()); + } + + protected static final Component getComponentWithFactoryId(List components, String factoryId) { + return components.stream().filter(t -> t.getFactoryId().equals(factoryId)).findFirst().orElse(null); + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppAssistant.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppAssistant.java index 121c9623d7a..3f46f03197f 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppAssistant.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppAssistant.java @@ -73,9 +73,9 @@ public static Builder create(String appname) { return new Builder().setAppName(appname); } - private final String name; - private final String alias; - private final JsonArray fields; + public final String name; + public final String alias; + public final JsonArray fields; private AppAssistant(String name, String alias, JsonArray fields) { this.name = name; diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppConfiguration.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppConfiguration.java index b13a565a2d5..1274e394c56 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppConfiguration.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppConfiguration.java @@ -4,6 +4,7 @@ import java.util.List; import io.openems.common.types.EdgeConfig.Component; +import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; public class AppConfiguration { // the components the app needs @@ -13,6 +14,12 @@ public class AppConfiguration { // the static ips in the Network configuration to access different networks public final List ips; + public final List dependencies; + + public AppConfiguration() { + this(null); + } + public AppConfiguration(List components) { this(components, null); } @@ -22,8 +29,14 @@ public AppConfiguration(List components, List schedulerExecut } public AppConfiguration(List components, List schedulerExecutionOrder, List ips) { - this.components = components; + this(components, schedulerExecutionOrder, ips, null); + } + + public AppConfiguration(List components, List schedulerExecutionOrder, List ips, + List dependencies) { + this.components = components != null ? components : new ArrayList<>(); this.schedulerExecutionOrder = schedulerExecutionOrder != null ? schedulerExecutionOrder : new ArrayList<>(); this.ips = ips != null ? ips : new ArrayList<>(); + this.dependencies = dependencies != null ? dependencies : new ArrayList<>(); } } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppInstallWorker.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppInstallWorker.java new file mode 100644 index 00000000000..1e9d7d6cf21 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppInstallWorker.java @@ -0,0 +1,66 @@ +package io.openems.edge.core.appmanager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.utils.JsonUtils; +import io.openems.common.worker.AbstractWorker; +import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; + +public class AppInstallWorker extends AbstractWorker { + + /** + * Time to wait before doing the check. This allows the system to completely + * boot and read configurations. And enough time to allow the user to delete the + * ReadOnly App and let him install the ReadWrite ones. + */ + private static final int INITIAL_WAIT_TIME = 60_000; // in ms + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private final AppManagerImpl parent; + + public AppInstallWorker(AppManagerImpl parent) { + this.parent = parent; + } + + private void installFreeApps() { + this.installReadOnlyApi("App.Api.ModbusTcp.ReadOnly", "App.Api.ModbusTcp.ReadWrite", + "Controller.Api.ModbusTcp.ReadWrite"); + this.installReadOnlyApi("App.Api.RestJson.ReadOnly", "App.Api.RestJson.ReadWrite", + "Controller.Api.Rest.ReadWrite"); + } + + private final void installReadOnlyApi(String readOnly, String readWrite, String readWriteController) { + if (this.parent.getInstantiatedApps().stream() + .noneMatch(t -> t.appId.equals(readOnly) || t.appId.equals(readWrite))) { + + // TODO this is only required if the ReadWrite controller exists without an App + if (this.parent.componentManager.getEdgeConfig().getComponentIdsByFactory(readWriteController) + .size() == 0) { + + try { + this.parent.handleAddAppInstanceRequest(null, + new AddAppInstance.Request(readOnly, "", JsonUtils.buildJsonObject().build())); + } catch (OpenemsNamedException e) { + this.log.info("Unable to install free App[" + readOnly + "]"); + } + } else { + this.log.warn("Unable to create App[" + readOnly + "] because a " + "Component with the FactoryId[" + + readWrite + "] exists!"); + } + } + } + + @Override + protected void forever() throws Throwable { + this.installFreeApps(); + } + + @Override + protected int getCycleTime() { + return INITIAL_WAIT_TIME; + } + +} 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 5106e95ea0b..3bd332584d2 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 @@ -1,19 +1,17 @@ package io.openems.edge.core.appmanager; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.Map.Entry; +import java.util.NoSuchElementException; import java.util.UUID; -import java.util.Vector; -import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.osgi.service.cm.ConfigurationAdmin; @@ -21,13 +19,13 @@ import org.osgi.service.cm.ConfigurationListener; 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.Deactivate; import org.osgi.service.component.annotations.Modified; import org.osgi.service.component.annotations.Reference; import org.osgi.service.metatype.annotations.Designate; import com.google.gson.JsonArray; -import com.google.gson.JsonObject; import io.openems.common.exceptions.OpenemsError; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; @@ -35,18 +33,17 @@ import io.openems.common.jsonrpc.base.GenericJsonrpcResponseSuccess; import io.openems.common.jsonrpc.base.JsonrpcRequest; import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; -import io.openems.common.jsonrpc.request.CreateComponentConfigRequest; -import io.openems.common.jsonrpc.request.DeleteComponentConfigRequest; import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest; import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest.Property; import io.openems.common.session.Role; -import io.openems.common.types.EdgeConfig.Component; import io.openems.common.utils.JsonUtils; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.jsonapi.JsonApi; import io.openems.edge.common.user.User; +import io.openems.edge.core.appmanager.dependency.AppManagerAppHelper; +import io.openems.edge.core.appmanager.dependency.Dependency; import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; import io.openems.edge.core.appmanager.jsonrpc.DeleteAppInstance; import io.openems.edge.core.appmanager.jsonrpc.GetApp; @@ -55,9 +52,11 @@ import io.openems.edge.core.appmanager.jsonrpc.GetAppInstances; import io.openems.edge.core.appmanager.jsonrpc.GetApps; import io.openems.edge.core.appmanager.jsonrpc.UpdateAppInstance; +import io.openems.edge.core.appmanager.validator.Validator; +import io.openems.edge.core.componentmanager.ComponentManagerImpl; @Designate(ocd = Config.class, factory = false) -@org.osgi.service.component.annotations.Component(// +@Component(// name = AppManager.SINGLETON_SERVICE_PID, // immediate = true, // property = { // @@ -67,18 +66,26 @@ public class AppManagerImpl extends AbstractOpenemsComponent implements AppManager, OpenemsComponent, JsonApi, ConfigurationListener { private final AppValidateWorker worker; + private final AppInstallWorker appInstallWorker; @Reference private ConfigurationAdmin cm; + @Reference protected List availableApps; + @Reference + private AppManagerAppHelper appHelper; + @Reference protected ComponentManager componentManager; @Reference protected ComponentUtil componentUtil; + @Reference + protected Validator validator; + protected final List instantiatedApps = new ArrayList<>(); public AppManagerImpl() { @@ -87,6 +94,7 @@ public AppManagerImpl() { AppManager.ChannelId.values() // ); this.worker = new AppValidateWorker(this); + this.appInstallWorker = new AppInstallWorker(this); } @Activate @@ -96,9 +104,10 @@ private void activate(ComponentContext componentContext, Config config) { if (OpenemsComponent.validateSingleton(this.cm, SINGLETON_SERVICE_PID, SINGLETON_COMPONENT_ID)) { return; } + this.applyConfig(config); this.worker.activate(this.id()); - this.applyConfig(config); + this.appInstallWorker.activate(this.id()); } /** @@ -117,11 +126,8 @@ public final List getInstantiatedApps() { * @return formated apps string */ private static String getJsonAppsString(List apps) { - var appsProperty = JsonUtils.buildJsonArray(); - for (var app : apps) { - appsProperty.add(app.toJsonObject()); - } - return JsonUtils.prettyToString(appsProperty.build()); + return JsonUtils + .prettyToString(apps.stream().map(OpenemsAppInstance::toJsonObject).collect(JsonUtils.toJsonArray())); } /** @@ -144,7 +150,18 @@ private static List parseInstantiatedApps(JsonArray apps) th errors.add("App with ID[" + instanceId + "] already exists!"); continue; } - result.add(new OpenemsAppInstance(appId, alias, instanceId, properties)); + List dependecies = null; + if (json.has("dependencies")) { + dependecies = new LinkedList<>(); + var dependecyArray = json.get("dependencies").getAsJsonArray(); + for (var i = 0; i < dependecyArray.size(); i++) { + var dependecyJson = dependecyArray.get(i).getAsJsonObject(); + var dependecy = new Dependency(dependecyJson.get("key").getAsString(), + JsonUtils.getAsUUID(dependecyJson, "instanceId")); + dependecies.add(dependecy); + } + } + result.add(new OpenemsAppInstance(appId, alias, instanceId, properties, dependecies)); } if (!errors.isEmpty()) { throw new OpenemsException(errors.stream().collect(Collectors.joining("|"))); @@ -173,273 +190,167 @@ private synchronized void applyConfig(Config config) { } } - protected void checkStatus(OpenemsApp openemsApp) throws OpenemsNamedException { - var validator = openemsApp.getValidator(); - var status = validator.getStatus(); - switch (validator.getStatus()) { - case INCOMPATIBLE: - throw new OpenemsException("App is not compatible! " - + validator.getErrorCompatibleMessages().stream().collect(Collectors.joining(";"))); - case COMPATIBLE: - throw new OpenemsException("App can not be installed! " - + validator.getErrorCompatibleMessages().stream().collect(Collectors.joining(";"))); - case INSTALLABLE: - // continue - break; - default: - throw new OpenemsException("Status '" + status.name() + "' is not implemented."); - } - } - @Override public void configurationEvent(ConfigurationEvent event) { this.worker.configurationEvent(event); } - private void foreachAppConfiguration(Consumer consumer, UUID... excludingInstanceIds) { - for (var appInstance : this.instantiatedApps) { - var skipInstance = false; - for (var id : excludingInstanceIds) { - if (Objects.equals(id, appInstance.instanceId)) { - skipInstance = true; - break; - } - } - if (skipInstance) { - continue; - } - var app = this.findAppById(appInstance.appId); - try { - consumer.accept(app.getAppConfiguration(ConfigurationTarget.VALIDATE, appInstance.properties)); - } catch (OpenemsNamedException e) { - // move to next app - } - } - } - - private void createComponent(User user, Component comp) throws OpenemsNamedException { - List properties = comp.getProperties().entrySet().stream() - .map(t -> new Property(t.getKey(), t.getValue())).collect(Collectors.toList()); - properties.add(new Property("id", comp.getId())); - properties.add(new Property("alias", comp.getAlias())); - - this.componentManager.handleJsonrpcRequest(user, - new CreateComponentConfigRequest(comp.getFactoryId(), properties)); - } - - @Override - @Deactivate - protected void deactivate() { - super.deactivate(); - this.worker.deactivate(); + /** + * Gets a filter for excluding instances. + * + * @param excludingInstanceIds the instances that should be excluded + * @return the filter + */ + public static Predicate exludingInstanceIds(UUID... excludingInstanceIds) { + return i -> !Arrays.stream(excludingInstanceIds).anyMatch(id -> id.equals(i.instanceId)); } - @Override - public String debugLog() { - return this.worker.debugLog(); + /** + * Gets an {@link Iterable} that loops thru every existing app instance and its + * configuration. + * + * @return the {@link Iterable} + */ + public Iterable> appConfigs() { + return this.appConfigs(null); } /** - * deletes the given components only if they are not in notMyComponents. + * Gets an {@link Iterable} that loops thru every existing app instance and its + * configuration. * - * @param user the executing user - * @param components the components that should be deleted - * @param notMyComponents other needed components from the other apps - * @return the id s of the components that got deleted + * @param filter the filter that gets applied to the instances + * @return the {@link Iterable} */ - private List deleteComponents(User user, List components, List notMyComponents) - throws OpenemsNamedException { - List errors = new ArrayList<>(); - List deletedIds = new ArrayList<>(); - for (var comp : components) { - if (notMyComponents.stream().parallel().anyMatch(t -> t.getId().equals(comp.getId()))) { - continue; - } - var component = this.componentManager.getEdgeConfig().getComponent(comp.getId()).orElse(null); - if (component == null) { - // component does not exist - continue; - } - - try { - this.componentManager.handleJsonrpcRequest(user, new DeleteComponentConfigRequest(comp.getId())); - deletedIds.add(comp.getId()); - } catch (OpenemsNamedException e) { - errors.add(e.toString()); - } - } - - if (!errors.isEmpty()) { - throw new OpenemsException(errors.stream().collect(Collectors.joining("|"))); - } - return deletedIds; + public Iterable> appConfigs( + Predicate filter) { + return this.appConfigs(this.instantiatedApps, filter); } /** - * finds the app with the matching id. + * Gets an {@link Iterable} that loops thru every instance and its + * configuration. * - * @param id of the app - * @return the found app + * @param instances the instances + * @param filter the filter that gets applied to the instances + * @return the {@link Iterable} */ - public final OpenemsApp findAppById(String id) { - return this.availableApps.stream() // - .filter(t -> t.getAppId().equals(id)) // - .findFirst() // - .get(); + public Iterable> appConfigs(List instances, + Predicate filter) { + return new Iterable<>() { + @Override + public Iterator> iterator() { + return AppManagerImpl.this.appConfigIterator(instances, filter); + } + }; } /** - * Gets an App Configuration with component id s, which can be used to create or - * rewrite the settings of the component. + * Gets an {@link Iterator} that loops thru every instance and its + * configuration. * - * @param app the {@link OpenemsApp} - * @param oldAppInstance the old {@link OpenemsAppInstance} - * @param newAppInstance the new {@link OpenemsAppInstance} - * @param otherAppComponents the components that are used from the other - * {@link OpenemsAppInstance} - * @return the AppConfiguration with the replaced ID s of the components - * @throws OpenemsNamedException on error + * @param instances the instances + * @param filter the filter that gets applied to the instances + * @return the {@link Iterator} */ - private AppConfiguration getNewAppConfigWithReplacedIds(OpenemsApp app, OpenemsAppInstance oldAppInstance, - OpenemsAppInstance newAppInstance, List otherAppComponents) throws OpenemsNamedException { - - var target = oldAppInstance == null ? ConfigurationTarget.ADD : ConfigurationTarget.UPDATE; - var newAppConfig = app.getAppConfiguration(target, newAppInstance.properties); - - final var replacableIds = this.getReplaceableComponentIds(app, newAppInstance.properties); - - for (var comp : ComponentUtilImpl.order(newAppConfig.components)) { - // replace old id s with new ones - for (var entry : comp.getProperties().entrySet()) { - for (var replaceableId : replacableIds.entrySet()) { - if (entry.getValue().toString().contains(replaceableId.getKey())) { - var newId = entry.getValue().toString().replace(replaceableId.getKey(), - newAppInstance.properties.get(replaceableId.getValue()).getAsString()); - newId = newId.replace("\"", ""); - var newValue = JsonUtils.getAsJsonElement(newId); - comp.getProperties().put(entry.getKey(), newValue); - } - } - } + private Iterator> appConfigIterator(List instances, + Predicate filter) { + List actualInstances = instances.stream().filter(i -> filter == null || filter.test(i)) // + .collect(Collectors.toList()); + return new Iterator<>() { - var isNewComponent = true; - var id = comp.getId(); - var canBeReplaced = replacableIds.containsKey(id); - Component foundComponent = null; + private final Iterator instanceIterator = actualInstances.iterator(); - // try to find a component with the necessary settings - if (canBeReplaced) { - foundComponent = this.componentUtil.getComponentByConfig(comp); - if (foundComponent != null) { - id = foundComponent.getId(); - } + private OpenemsAppInstance nextInstance = null; + private AppConfiguration nextConfiguration = null; + + @Override + public Entry next() { + var returnValue = new AbstractMap.SimpleEntry<>(this.nextInstance, this.nextConfiguration); + this.nextInstance = null; + this.nextConfiguration = null; + return returnValue; } - if (foundComponent == null && oldAppInstance != null && oldAppInstance.properties.has(id.toUpperCase())) { - id = oldAppInstance.properties.get(id.toUpperCase()).getAsString(); - foundComponent = this.componentManager.getEdgeConfig().getComponent(id).orElse(null); - final var tempId = id; - // other app uses the same component because they had the same configuration - // now this app needs the component with a different configuration so now create - // a new component - if (foundComponent != null && otherAppComponents.stream().anyMatch(t -> t.getId().equals(tempId))) { - foundComponent = null; + + @Override + public boolean hasNext() { + if (this.nextConfiguration == null && !this.instanceIterator.hasNext()) { + return false; } - } - isNewComponent = isNewComponent && foundComponent == null; - if (isNewComponent) { - // if the id is not already set and there is no component with the default id - // then use the default id - foundComponent = this.componentManager.getEdgeConfig().getComponent(comp.getId()).orElse(null); - if (foundComponent == null) { - id = comp.getId(); - } else { - // replace number at the end and get the next available id - var nextAvailableId = this.componentUtil.getNextAvailableId(id.replaceAll("\\d+", ""), - otherAppComponents); - if (!nextAvailableId.equals(id) && !canBeReplaced) { - // component can not be created because the id is already used - // and the id can not be set in the configuration - continue; - } - if (canBeReplaced) { - id = nextAvailableId; - } + this.nextInstance = this.instanceIterator.next(); + + try { + var app = AppManagerImpl.this.findAppById(this.nextInstance.appId); + this.nextInstance.properties.addProperty("ALIAS", this.nextInstance.alias); + this.nextConfiguration = app.getAppConfiguration(ConfigurationTarget.VALIDATE, + this.nextInstance.properties, null); + this.nextInstance.properties.remove("ALIAS"); + } catch (OpenemsNamedException e) { + // move to next app + } catch (NoSuchElementException e) { + // app not found for instance + // this may happen if the app id gets refactored + // apps which app ids are not known are printed in debug log as 'UNKNOWAPPS' } - } - if (canBeReplaced) { - newAppInstance.properties.addProperty(replacableIds.get(comp.getId()), id); + return this.nextConfiguration != null; } - } - return app.getAppConfiguration(target, newAppInstance.properties); + }; + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + this.worker.deactivate(); + this.appInstallWorker.deactivate(); + } + + @Override + public String debugLog() { + return this.worker.debugLog(); } /** - * Gets the components of all apps except the given. + * finds the app with the matching id. * - * @param thisApp the app that components should not be included - * @return all components from all app instances except the given thisApp + * @param id of the app + * @return the found app */ - private List getOtherAppComponents(OpenemsAppInstance thisApp) { - List allOtherComponents = new ArrayList<>(); - this.foreachAppConfiguration(c -> { - allOtherComponents.addAll(c.components); - }, thisApp.instanceId); - return allOtherComponents; + public final OpenemsApp findAppById(String id) throws NoSuchElementException { + return this.availableApps.stream() // + .filter(t -> t.getAppId().equals(id)) // + .findFirst() // + .get(); } /** - * Gets ip s that are needed from the other {@link OpenemsAppInstance}s. + * Finds the app instance with the matching id. * - * @param thisApp the app which ip s should not be included - * @return all needed ip s from the other apps + * @param uuid the id of the instance + * @return s the instance + * @throws NoSuchElementException if no instance is present */ - private List getOtherAppIps(OpenemsAppInstance thisApp) { - List allOtherIps = new ArrayList<>(); - this.foreachAppConfiguration(c -> { - allOtherIps.addAll(c.ips); - }, thisApp.instanceId); - return allOtherIps; + public final OpenemsAppInstance findInstanceById(UUID uuid) throws NoSuchElementException { + return this.instantiatedApps.stream() // + .filter(t -> t.instanceId.equals(uuid)) // + .findFirst() // + .get(); } /** - * Gets the component id s that can be replaced. + * Gets all {@link AppConfiguration}s from the existing + * {@link OpenemsAppInstance}s. * - * @param app the components of which app - * @param properties the default properties to create an app instance of this - * app - * @return a map of the component id s that can be replaced mapped from id to - * key to put the next id - * @throws OpenemsNamedException on error + * @param ignoreIds the id's of the instances that should be ignored + * @return the {@link AppConfiguration}s */ - protected final Map getReplaceableComponentIds(OpenemsApp app, JsonObject properties) - throws OpenemsNamedException { - final var prefix = "?_?_"; - var config = app.getAppConfiguration(ConfigurationTarget.TEST, properties); - var copyBuilder = JsonUtils.buildJsonObject(); - for (var entry : properties.entrySet()) { - copyBuilder.add(entry.getKey(), entry.getValue()); - } - for (var comp : config.components) { - copyBuilder.addProperty(comp.getId(), prefix); - } - var copy = copyBuilder.build(); - var configWithNewIds = app.getAppConfiguration(ConfigurationTarget.TEST, copy); - Map replaceableComponentIds = new HashMap<>(); - for (var comp : configWithNewIds.components) { - if (comp.getId().startsWith(prefix)) { - // "METER_ID:meter0" - var raw = comp.getId().substring(prefix.length()); - // ["METER_ID", "meter0"] - var pieces = raw.split(":"); - // "METER_ID" - var property = pieces[0]; - // "meter0" - var defaultId = pieces[1]; - replaceableComponentIds.put(defaultId, property); - } + public final List getOtherAppConfigurations(UUID... ignoreIds) { + List allOtherConfigs = new ArrayList<>(this.instantiatedApps.size()); + for (var entry : this.appConfigs(AppManagerImpl.exludingInstanceIds(ignoreIds))) { + allOtherConfigs.add(entry.getValue()); } - return replaceableComponentIds; + return allOtherConfigs; } /** @@ -450,40 +361,27 @@ protected final Map getReplaceableComponentIds(OpenemsApp app, J * @return the Future JSON-RPC Response * @throws OpenemsNamedException on error */ - private CompletableFuture handleAddAppInstanceRequest(User user, + public CompletableFuture handleAddAppInstanceRequest(User user, AddAppInstance.Request request) throws OpenemsNamedException { - var instanceId = UUID.randomUUID(); - var openemsApp = this.findAppById(request.appId); synchronized (this.instantiatedApps) { - this.checkStatus(openemsApp); - - // create app instance - var app = new OpenemsAppInstance(request.appId, request.alias, instanceId, request.properties); - List errors = new Vector<>(); - var completable = this.updateAppSettings(errors, user, openemsApp, null, app); + var installedValues = this.appHelper.installApp(user, request.properties, request.alias, openemsApp); - try { - // wait until everything is finished - completable.get(); - } catch (ExecutionException | CancellationException | InterruptedException e) { - errors.add(e.getMessage()); - } - if (!errors.isEmpty()) { - throw new OpenemsException(errors.stream().collect(Collectors.joining("|"))); - } // Update App-Manager configuration try { - this.instantiatedApps.add(app); + // replace old instances with new ones + this.instantiatedApps.removeAll(installedValues.modifiedOrCreatedApps); + this.instantiatedApps.addAll(installedValues.modifiedOrCreatedApps); this.updateAppManagerConfiguration(user, this.instantiatedApps); } catch (OpenemsNamedException e) { throw new OpenemsException( "AddAppInstance: unable to update App-Manager configuration: " + e.getMessage()); } + return CompletableFuture.completedFuture( + new AddAppInstance.Response(request.id, installedValues.rootInstance, installedValues.warnings)); } - return CompletableFuture.completedFuture(new AddAppInstance.Response(request.id, instanceId)); } /** @@ -494,63 +392,32 @@ private CompletableFuture handleAddAppInstanceRequest(Us * @return the request id * @throws OpenemsNamedException on error */ - private CompletableFuture handleDeleteAppInstanceRequest(User user, + public CompletableFuture handleDeleteAppInstanceRequest(User user, DeleteAppInstance.Request request) throws OpenemsNamedException { synchronized (this.instantiatedApps) { - final var instance = this.instantiatedApps.stream().filter(t -> t.instanceId.equals(request.instanceId)) - .findFirst().orElse(null); - if (instance == null) { + final OpenemsAppInstance instance; + try { + instance = this.findInstanceById(request.instanceId); + } catch (NoSuchElementException e) { return CompletableFuture.completedFuture(new GenericJsonrpcResponseSuccess(request.id)); } - var app = this.findAppById(instance.appId); - var config = app.getAppConfiguration(ConfigurationTarget.DELETE, instance.properties); - List errors = new Vector<>(); - var deleteComponents = CompletableFuture.runAsync(() -> { - try { - var deletedIds = this.deleteComponents(user, config.components, - this.getOtherAppComponents(instance)); - deletedIds.addAll(config.schedulerExecutionOrder); - this.componentUtil.removeIdsInSchedulerIfExisting(user, deletedIds); - } catch (OpenemsNamedException e) { - errors.add(e); - } - }); - // TODO remove 'if' if it works on windows - // rewriting network configuration only works on Linux - var updateNetworkConfig = CompletableFuture.runAsync(() -> { - if (!System.getProperty("os.name").startsWith("Windows")) { - var ips = new ArrayList<>(config.ips); - ips.removeAll(this.getOtherAppIps(instance)); - try { - this.componentUtil.updateHosts(user, null, ips); - } catch (OpenemsNamedException e) { - errors.add(e); - } - } - }); - try { - // wait until everything is finished - CompletableFuture.allOf(deleteComponents, updateNetworkConfig).get(); - } catch (ExecutionException | CancellationException | InterruptedException e) { - errors.add(new OpenemsException(e.toString())); - } + var result = this.appHelper.deleteApp(user, instance); + try { - this.instantiatedApps.remove(instance); + this.instantiatedApps.removeAll(result.deletedApps); + // replace modified apps + this.instantiatedApps.removeAll(result.modifiedOrCreatedApps); + this.instantiatedApps.addAll(result.modifiedOrCreatedApps); this.updateAppManagerConfiguration(user, this.instantiatedApps); } catch (OpenemsNamedException e) { - errors.add(new OpenemsException(e.toString())); - } - - if (!errors.isEmpty()) { - throw new OpenemsException( - errors.stream().map(OpenemsNamedException::toString).collect(Collectors.joining("|"))); + throw new OpenemsException("Unable to update App-Manager configuration for ID [" + request.instanceId + + "]: " + e.getMessage()); } + return CompletableFuture.completedFuture(new DeleteAppInstance.Response(request.id, result.warnings)); } - - return CompletableFuture.completedFuture(new GenericJsonrpcResponseSuccess(request.id)); } /** @@ -565,8 +432,8 @@ private CompletableFuture handleGetAppAssistantRequest(U GetAppAssistant.Request request) throws OpenemsNamedException { for (var app : this.availableApps) { if (request.appId.equals(app.getAppId())) { - return CompletableFuture - .completedFuture(new GetAppAssistant.Response(request.id, app.getAppAssistant())); + return CompletableFuture.completedFuture( + new GetAppAssistant.Response(request.id, app.getAppAssistant(user.getLanguage()))); } } throw new OpenemsException("App-ID [" + request.appId + "] is unknown"); @@ -582,13 +449,12 @@ private CompletableFuture handleGetAppAssistantRequest(U */ private CompletableFuture handleGetAppDescriptorRequest(User user, GetAppDescriptor.Request request) throws OpenemsNamedException { - for (var app : this.availableApps) { - if (request.appId.equals(app.getAppId())) { - return CompletableFuture - .completedFuture(new GetAppDescriptor.Response(request.id, app.getAppDescriptor())); - } + try { + var app = this.findAppById(request.appId); + return CompletableFuture.completedFuture(new GetAppDescriptor.Response(request.id, app.getAppDescriptor())); + } catch (NoSuchElementException e) { + throw new OpenemsException("App-ID [" + request.appId + "] is unknown"); } - throw new OpenemsException("App-ID [" + request.appId + "] is unknown"); } /** @@ -620,7 +486,8 @@ private CompletableFuture handleGetAppRequest(User user, var app = this.availableApps.stream().filter(t -> t.getAppId().equals(request.appId)).findFirst().get(); var instances = this.instantiatedApps.stream().filter(t -> t.appId.equals(request.appId)) .collect(Collectors.toList()); - return CompletableFuture.completedFuture(new GetApp.Response(request.id, app, instances)); + return CompletableFuture + .completedFuture(new GetApp.Response(request.id, app, instances, user.getLanguage(), this.validator)); } /** @@ -633,8 +500,8 @@ private CompletableFuture handleGetAppRequest(User user, */ private CompletableFuture handleGetAppsRequest(User user, GetApps.Request request) throws OpenemsNamedException { - return CompletableFuture - .completedFuture(new GetApps.Response(request.id, this.availableApps, this.instantiatedApps)); + return CompletableFuture.completedFuture(new GetApps.Response(request.id, this.availableApps, + this.instantiatedApps, user.getLanguage(), this.validator)); } @Override @@ -683,39 +550,35 @@ public CompletableFuture handleJsonrpcRequest( */ private CompletableFuture handleUpdateAppInstanceRequest(User user, UpdateAppInstance.Request request) throws OpenemsNamedException { - OpenemsAppInstance newApp = null; - OpenemsAppInstance oldApp = null; + synchronized (this.instantiatedApps) { - for (var app : this.instantiatedApps) { - if (app.instanceId.equals(request.instanceId)) { - oldApp = app; - newApp = new OpenemsAppInstance(app.appId, request.alias, app.instanceId, request.properties); - break; - } - } + OpenemsAppInstance oldApp = null; + OpenemsApp app = null; - if (newApp == null) { + try { + oldApp = this.findInstanceById(request.instanceId); + app = this.findAppById(oldApp.appId); + } catch (NoSuchElementException e) { throw new OpenemsException("App-Instance-ID [" + request.instanceId + "] is unknown."); } - var errors = this.reconfigurApp(user, oldApp, newApp); + var result = this.appHelper.updateApp(user, oldApp, request.properties, request.alias, app); // Update App-Manager configuration try { - this.instantiatedApps.remove(oldApp); - this.instantiatedApps.add(newApp); + this.instantiatedApps.removeAll(result.deletedApps); + // replace old instances with new ones + this.instantiatedApps.removeAll(result.modifiedOrCreatedApps); + this.instantiatedApps.addAll(result.modifiedOrCreatedApps); this.updateAppManagerConfiguration(user, this.instantiatedApps); } catch (OpenemsNamedException e) { throw new OpenemsException("Unable to update App-Manager configuration for ID [" + request.instanceId + "]: " + e.getMessage()); } - - if (!errors.isEmpty()) { - throw new OpenemsException(errors.stream().collect(Collectors.joining("|"))); - } - + var newInstance = this.findInstanceById(request.instanceId); + return CompletableFuture + .completedFuture(new UpdateAppInstance.Response(request.id, newInstance, result.warnings)); } - return CompletableFuture.completedFuture(new GenericJsonrpcResponseSuccess(request.id)); } @Modified @@ -725,51 +588,6 @@ private void modified(ComponentContext componentContext, Config config) throws O this.worker.triggerNextRun(); } - /** - * Reconfigurates an app instance. - * - * @param user the executing user - * @param oldAppInstance the old app instance with the old configuration - * @param newAppInstance the new app instance with the new configuration - * @return the errors that occurred during reconfiguration - */ - private List reconfigurApp(User user, OpenemsAppInstance oldAppInstance, OpenemsAppInstance newAppInstance) - throws OpenemsNamedException { - var app = this.findAppById(newAppInstance.appId); - List errors = new Vector<>(); - var completable = this.updateAppSettings(errors, user, app, oldAppInstance, newAppInstance); - try { - // wait until everything is finished - completable.get(); - } catch (ExecutionException | CancellationException | InterruptedException e) { - errors.add(e.getMessage()); - } - return errors; - } - - /** - * checks if the settings of the component changed if there is a change it - * rewrites the settings of the given component. - * - * @param user the executing user - * @param myComp the component that configuration should be rewritten - * @param actualComp the actual component that exists - * @throws OpenemsNamedException when the configuration can not be rewritten - */ - private void reconfigure(User user, Component myComp, Component actualComp) throws OpenemsNamedException { - if (ComponentUtilImpl.isSameConfiguration(null, myComp, actualComp)) { - return; - } - - // send update request - List properties = myComp.getProperties().entrySet().stream() - .map(t -> new Property(t.getKey(), t.getValue())) // - .collect(Collectors.toList()); - properties.add(new Property("alias", myComp.getAlias())); - var updateRequest = new UpdateComponentConfigRequest(actualComp.getId(), properties); - this.componentManager.handleJsonrpcRequest(user, updateRequest); - } - /** * updated the AppManager configuration with the given app instances. * @@ -780,183 +598,11 @@ private void reconfigure(User user, Component myComp, Component actualComp) thro private void updateAppManagerConfiguration(User user, List apps) throws OpenemsNamedException { var p = new Property("apps", getJsonAppsString(apps)); var updateRequest = new UpdateComponentConfigRequest(SINGLETON_COMPONENT_ID, Arrays.asList(p)); - this.componentManager.handleJsonrpcRequest(user, updateRequest); - } - - /** - * creates the needed components of the given app with the given config and - * updates components with a new configuration and deletes unused components. - * - * @param errorList a list for the errors that occur - * @param user the executing user - * @param app the app that should be created - * @param oldAppInstance the old app instance - * @param newAppInstance the new app instance - * @return the completableFuture of this task - */ - public CompletableFuture updateAppSettings(List errorList, User user, OpenemsApp app, - OpenemsAppInstance oldAppInstance, OpenemsAppInstance newAppInstance) throws OpenemsNamedException { - final List errors; - if (errorList == null) { - errors = new Vector<>(); + // user can be null using internal method + if (user == null) { + ((ComponentManagerImpl) this.componentManager).handleUpdateComponentConfigRequest(user, updateRequest); } else { - errors = errorList; - } - AppConfiguration oldAppConfigTemp = null; - if (oldAppInstance != null) { - oldAppInstance.properties.addProperty("ALIAS", oldAppInstance.alias); - try { - oldAppConfigTemp = app.getAppConfiguration(ConfigurationTarget.VALIDATE, oldAppInstance.properties); - } catch (OpenemsNamedException ex) { - errors.add(ex.getMessage()); - } + this.componentManager.handleJsonrpcRequest(user, updateRequest); } - final var oldAppConfig = oldAppConfigTemp; - // adding alias to the properties in order to access it while defining it in the - // App Configuration - newAppInstance.properties.addProperty("ALIAS", newAppInstance.alias); - final var otherComponents = this.getOtherAppComponents(newAppInstance); - final var newAppConfig = this.getNewAppConfigWithReplacedIds(app, oldAppInstance, newAppInstance, - otherComponents); - - // TODO remove 'if' if it works on windows - // rewriting network configuration only works on Linux - if (!System.getProperty("os.name").startsWith("Windows")) { - try { - this.componentUtil.updateHosts(user, newAppConfig.ips, oldAppConfig != null ? oldAppConfig.ips : null); - } catch (OpenemsNamedException e) { - var error = "Can not update Host Config"; - errors.add(error); - } - } - - try { - // validate input e. g. ping a specific ip - app.getValidator().validateConfiguration(ConfigurationTarget.ADD, newAppInstance.properties); - } catch (OpenemsNamedException ex) { - // revert network configuration - errors.add(ex.getMessage()); - return CompletableFuture.runAsync(() -> { - if (!System.getProperty("os.name").startsWith("Windows")) { - var ips = new ArrayList<>(newAppConfig.ips); - ips.removeAll(this.getOtherAppIps(newAppInstance)); - try { - this.componentUtil.updateHosts(user, null, ips); - } catch (OpenemsNamedException e) { - errors.add(e.getMessage()); - } - } - }); - } - - // adds / updates components - var updatingComponents = CompletableFuture.runAsync(() -> { - var createdComponents = new LinkedList(); - // create components - for (Component comp : ComponentUtilImpl.order(newAppConfig.components)) { - /** - * if comp already exists with same config as needed => use it. if comp exist - * with different config and no other app needs it => rewrite settings. if comp - * exist with different config and other app needs it => create new comp - */ - var foundComponentWithSameId = this.componentManager.getEdgeConfig().getComponent(comp.getId()) - .orElse(null); - if (oldAppConfig != null) { - oldAppConfig.components.removeIf(t -> t.getId().equals(comp.getId())); - } - if (foundComponentWithSameId != null) { - - var isSameConfigWithoutAlias = ComponentUtilImpl.isSameConfigurationWithoutAlias(null, comp, - foundComponentWithSameId); - var isSameConfig = isSameConfigWithoutAlias - && comp.getAlias().equals(foundComponentWithSameId.getAlias()); - - if (isSameConfig) { - // same configuration so no reconfiguration needed - continue; - } - - // check if it is my component - if (otherComponents.stream().anyMatch(t -> t.getId().equals(foundComponentWithSameId.getId()))) { - // not my component but only the alias changed - if (isSameConfigWithoutAlias) { - // TODO maybe warning if the alias can't be set - continue; - } - errors.add("Configuration of component with id '" + foundComponentWithSameId.getId() - + "' can not be rewritten. Because the component belongs to another app."); - continue; - } - try { - this.reconfigure(user, comp, foundComponentWithSameId); - } catch (OpenemsNamedException e) { - errors.add(e.getMessage()); - } - continue; - } - - // create new component - try { - this.createComponent(user, comp); - createdComponents.add(comp); - } catch (OpenemsNamedException e) { - var error = "Component[" + comp.getFactoryId() + "] cant be created!"; - errors.add(error); - errors.add(e.getMessage()); - } - - } - - // update scheduler - try { - var schedulerOrder = new ArrayList<>(newAppConfig.schedulerExecutionOrder); - // if another app needs this component for the scheduler now add it - if (createdComponents.isEmpty()) { - this.foreachAppConfiguration(c -> { - - // if any component id is included - if (!createdComponents.stream().anyMatch(t -> c.schedulerExecutionOrder.contains(t.getId()))) { - return; - } - - var temp = this.componentUtil.insertSchedulerOrder(schedulerOrder, c.schedulerExecutionOrder); - schedulerOrder.clear(); - schedulerOrder.addAll(temp); - - }); - } - this.componentUtil.updateScheduler(user, schedulerOrder, createdComponents); - } catch (OpenemsNamedException e) { - errors.add("Can't update scheduler execute order. Message: " + e.getMessage()); - } - - }); - - // deletes components that were used in the old configuration but are not in the - // new configuration - var updateSchedulerDeletingIds = updatingComponents.thenRunAsync(() -> { - if (oldAppConfig != null) { - try { - var deletedIds = this.deleteComponents(user, oldAppConfig.components, otherComponents); - oldAppConfig.schedulerExecutionOrder.removeAll(newAppConfig.schedulerExecutionOrder); - deletedIds.addAll(oldAppConfig.schedulerExecutionOrder); - this.componentUtil.removeIdsInSchedulerIfExisting(user, deletedIds); - } catch (OpenemsNamedException e) { - errors.add(e.getMessage()); - } - - } - }); - - // remove alias so it does not get written down twice in the app configuration - var removingAlias = updateSchedulerDeletingIds.thenRunAsync(() -> { - if (oldAppInstance != null) { - oldAppInstance.properties.remove("ALIAS"); - } - newAppInstance.properties.remove("ALIAS"); - }); - - return CompletableFuture.allOf(updatingComponents, updateSchedulerDeletingIds, removingAlias); } - } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/ComponentUtil.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/ComponentUtil.java index 474ef16110a..9824b5361dd 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/ComponentUtil.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/ComponentUtil.java @@ -1,6 +1,7 @@ package io.openems.edge.core.appmanager; import java.util.List; +import java.util.Optional; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.types.EdgeConfig; @@ -29,6 +30,14 @@ public interface ComponentUtil { */ public boolean anyComponentUses(String value, List ignoreIds); + /** + * Gets a list of current Relays. e. g. 'io0/Relay1' + * + * @return a list of Relays + * @throws OpenemsNamedException on error + */ + public List getAllRelays(); + /** * Gets a list of currently available Relays of IOs which are not used by any * component. e. g. 'io0/Relay1' @@ -201,4 +210,12 @@ public void updateScheduler(User user, List schedulerExecutionOrder, Lis */ public void updateHosts(User user, List ips, List oldIps) throws OpenemsNamedException; + /** + * Gets an {@link Optional} of an {@link EdgeConfig.Component}. + * + * @param id the id of the component + * @param factoryId the factoryId of the component + * @return the optional component + */ + public Optional getComponent(String id, String factoryId); } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/ComponentUtilImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/ComponentUtilImpl.java index 1af4211003e..4e48487b9ae 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/ComponentUtilImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/ComponentUtilImpl.java @@ -10,6 +10,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -376,6 +377,21 @@ public boolean anyComponentUses(String value, List ignoreIds) { }); } + @Override + public List getAllRelays() { + List allDigitalOutputs = this.getEnabledComponentsOfType(DigitalOutput.class); + List relays = new LinkedList<>(); + for (DigitalOutput digitalOutput : allDigitalOutputs) { + List availableIos = new LinkedList<>(); + for (var i = 0; i < digitalOutput.digitalOutputChannels().length; i++) { + var ioName = digitalOutput.id() + "/Relay" + (i + 1); + availableIos.add(ioName); + } + relays.add(new Relay(digitalOutput.id(), availableIos, digitalOutput.digitalOutputChannels().length)); + } + return relays; + } + @Override public List getAvailableRelays() { return this.getAvailableRelays(new ArrayList<>()); @@ -760,4 +776,16 @@ public void updateHosts(User user, List ips, List oldIps) throws } } + @Override + public Optional getComponent(String id, String factoryId) { + var comp = this.componentManager.getEdgeConfig().getComponent(id); + if (comp.isEmpty()) { + return Optional.empty(); + } + if (!comp.get().getFactoryId().equals(factoryId)) { + return Optional.empty(); + } + return comp; + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/JsonFormlyUtil.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/JsonFormlyUtil.java index 2e8fd04db55..56d76619ad4 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/JsonFormlyUtil.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/JsonFormlyUtil.java @@ -5,13 +5,17 @@ import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; +import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; 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.utils.JsonUtils; +import io.openems.common.utils.JsonUtils.JsonObjectBuilder; /** * Source https://formly.dev/examples/introduction. @@ -54,6 +58,17 @@ public static > SelectBuilder buildSelect(T property) { return new SelectBuilder(property); } + /** + * Creates a JsonObject Formly Repeat Builder for the given enum. + * + * @param the type of the enum + * @param property the enum property + * @return a {@link RepeatBuilder} + */ + public static > RepeatBuilder buildRepeat(T property) { + return new RepeatBuilder(property); + } + /** * A Builder for a Formly field. * @@ -98,8 +113,12 @@ private final T setType(String type) { return this.getSelf(); } - private final T setKey(String key) { - this.jsonObject.addProperty("key", key); + public final T setKey(String key) { + if (key != null) { + this.jsonObject.addProperty("key", key); + } else if (this.jsonObject.has("key")) { + this.jsonObject.remove("key"); + } return this.getSelf(); } @@ -133,6 +152,24 @@ public final T setDefaultValue(Number defaultValue) { return this.getSelf(); } + public final T setDefaultValue(JsonElement defaultValue) { + if (defaultValue != null) { + this.jsonObject.add("defaultValue", defaultValue); + } else if (this.jsonObject.has("defaultValue")) { + this.jsonObject.remove("defaultValue"); + } + + return this.getSelf(); + } + + public final T setDefaultValueWithStringSupplier(Supplier supplieDefaultValue) { + return this.setDefaultValue(supplieDefaultValue.get()); + } + + public final T setDefaultValueWithBooleanSupplier(Supplier supplieDefaultValue) { + return this.setDefaultValue(supplieDefaultValue.get()); + } + public final T isRequired(boolean isRequired) { if (isRequired) { this.templateOptions.addProperty("required", isRequired); @@ -143,7 +180,11 @@ public final T isRequired(boolean isRequired) { } public final T setLabel(String label) { - this.templateOptions.addProperty("label", label); + if (label != null) { + this.templateOptions.addProperty("label", label); + } else if (this.templateOptions.has("label")) { + this.templateOptions.remove("label"); + } return this.getSelf(); } @@ -152,12 +193,32 @@ public final T setDescription(String description) { return this.getSelf(); } + /** + * Call a method on a FormlyBuilder if the expression is true. + * + * @param expression the expression + * @param consumer allows a lambda function on {@link FormlyBuilder} + * @return the {@link JsonObjectBuilder} + */ + public T onlyIf(boolean expression, Consumer consumer) { + if (expression) { + consumer.accept(this.getSelf()); + } + return this.getSelf(); + } + public final > T onlyShowIfChecked(PROPERTEY property) { this.getExpressionProperties().addProperty("templateOptions.required", "model." + property.name()); this.jsonObject.addProperty("hideExpression", "!model." + property.name()); return this.getSelf(); } + public final > T onlyShowIfNotChecked(PROPERTEY property) { + this.getExpressionProperties().addProperty("templateOptions.required", "!model." + property.name()); + this.jsonObject.addProperty("hideExpression", "model." + property.name()); + return this.getSelf(); + } + public JsonObject build() { this.jsonObject.add("templateOptions", this.templateOptions); if (this.expressionProperties != null && this.expressionProperties.size() > 0) { @@ -521,6 +582,10 @@ public SelectBuilder setOptions(Set> items, Function items) { + return this.setOptions(items, t -> t, t -> t); + } + public SelectBuilder setOptions(List items, Function item2Label, Function item2Value) { var options = JsonUtils.buildJsonArray(); @@ -555,4 +620,65 @@ protected String getType() { } + /** + * A Builder for a Formly Checkbox. + * + *
+	 * {
+	 * 	"key": "key",
+	 * 	"type": "repeat",
+	 * 	"templateOptions": {
+	 * 		"label": "label",
+	 * 		"required": true
+	 * 	},
+	 * 	"expressionProperties": {
+	 * 		"templateOptions.required": "model.PROPERTY"
+	 * 	},
+	 * 	"hideExpression": "!model.PROPERTY",
+	 * 	"defaultValue": "defaultValue"
+	 * }
+	 * 
+ * + */ + public static final class RepeatBuilder extends FormlyBuilder { + + private JsonObject fieldArray; + + private > RepeatBuilder(PROPERTY property) { + super(property); + } + + private RepeatBuilder(DefaultEnum property) { + super(property); + } + + public RepeatBuilder setAddText(String addText) { + if (addText != null && addText.isBlank()) { + this.templateOptions.addProperty("addText", addText); + } else if (this.templateOptions.has("addText")) { + this.templateOptions.remove("addText"); + } + return this; + } + + public RepeatBuilder setFieldArray(JsonObject object) { + this.fieldArray = object; + return this; + } + + @Override + protected String getType() { + return "repeat"; + } + + @Override + public JsonObject build() { + if (this.fieldArray != null) { + this.jsonObject.add("fieldArray", this.fieldArray); + } + return super.build(); + } + + } + } 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 216b322d94d..79e141a1ac6 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 @@ -5,25 +5,28 @@ import com.google.gson.JsonObject; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.edge.core.appmanager.validator.Validator; +import io.openems.common.session.Language; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; public interface OpenemsApp { /** * Gets the {@link AppAssistant} for this {@link OpenemsApp}. * + * @param language the language of the {@link AppAssistant} * @return the AppAssistant */ - public AppAssistant getAppAssistant(); + public AppAssistant getAppAssistant(Language language); /** * Gets the {@link AppConfiguration} needed for the {@link OpenemsApp}. * - * @param target the {@link ConfigurationTarget} - * @param config the configured app 'properties' + * @param target the {@link ConfigurationTarget} + * @param config the configured app 'properties' + * @param language the language of the configuration * @return the app Configuration */ - public AppConfiguration getAppConfiguration(ConfigurationTarget target, JsonObject config) + public AppConfiguration getAppConfiguration(ConfigurationTarget target, JsonObject config, Language language) throws OpenemsNamedException; /** @@ -58,9 +61,10 @@ public AppConfiguration getAppConfiguration(ConfigurationTarget target, JsonObje /** * Gets the name of the {@link OpenemsApp}. * + * @param language the language of the name * @return a human readable name */ - public String getName(); + public String getName(Language language); /** * Gets the {@link OpenemsAppCardinality} of the {@link OpenemsApp}. @@ -70,11 +74,11 @@ public AppConfiguration getAppConfiguration(ConfigurationTarget target, JsonObje public OpenemsAppCardinality getCardinality(); /** - * Gets the {@link Validator} of this {@link OpenemsApp}. + * Gets the {@link ValidatorConfig} of this {@link OpenemsApp}. * - * @return the Validator + * @return the ValidatorConfig */ - public Validator getValidator(); + public ValidatorConfig getValidatorConfig(); /** * Validate the {@link OpenemsApp}. 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 382d511f80a..4f239238411 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 @@ -1,72 +1,100 @@ package io.openems.edge.core.appmanager; +import java.util.ResourceBundle; + import com.google.gson.JsonObject; +import io.openems.common.session.Language; import io.openems.common.utils.JsonUtils; public enum OpenemsAppCategory { - // TODO translation - /** * Integrated Systems. */ - INTEGRATED_SYSTEM("Integrierte Systeme"), + INTEGRATED_SYSTEM("integratedSystems"), /** - * Time of use energy tariff. + * Time variable energy price. */ - TIME_OF_USE_TARIFF("Zeitvariable Stromtarife"), + TIME_OF_USE_TARIFF("timeOfUseTariff"), /** * Electric vehicle charging station. */ - EVCS("E-Mobilität"), + EVCS("evcs"), /** - * Load control. + * Heat. */ - HEAT("Wärme"), + HEAT("heat"), + + /** + * Load Control. + */ + LOAD_CONTROL("loadControl"), /** * Hardware. */ - HARDWARE("Hardware"), + HARDWARE("hardware"), /** * PV-Inverter. */ - PV_INVERTER("PV-Wechselrichter"), + PV_INVERTER("pvInverter"), + + /** + * PV self-consumption. + */ + PV_SELF_CONSUMPTION("pvSelfConsumption"), /** * Meter. */ - METER("Zähler"), + METER("meter"), /** * Apis. */ - API("Schnittstellen"); + API("api"), + + /** + * Category for test apps. + * + *

+ * NOTE: Do not use this category for normal apps! + */ + TEST("test"); - private String readableName; + private String readableNameKey; - private OpenemsAppCategory(String readableName) { - this.readableName = readableName; + private OpenemsAppCategory(String readableNameKey) { + this.readableNameKey = readableNameKey; } - public String getReadableName() { - return this.readableName; + /** + * Gets the readable name in the specific language. + * + * @param language the language of the name + * @return the name + */ + public String getReadableName(Language language) { + var translationBundle = ResourceBundle.getBundle("io.openems.edge.core.appmanager.translation", + language.getLocal()); + return TranslationUtil.getTranslation(translationBundle, this.readableNameKey); } /** * Creates a {@link JsonObject} of the {@link OpenemsAppCategory}. * + * @param language the language of the readable name * @return the {@link JsonObject} */ - public JsonObject toJsonObject() { + public JsonObject toJsonObject(Language language) { return JsonUtils.buildJsonObject() // .addProperty("name", this.name()) // - .addProperty("readableName", this.getReadableName()) // + .addProperty("readableName", this.getReadableName(language)) // .build(); } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsAppInstance.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsAppInstance.java index 36d9402b3a0..92905193689 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsAppInstance.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsAppInstance.java @@ -1,11 +1,13 @@ package io.openems.edge.core.appmanager; +import java.util.List; import java.util.Objects; import java.util.UUID; import com.google.gson.JsonObject; import io.openems.common.utils.JsonUtils; +import io.openems.edge.core.appmanager.dependency.Dependency; /** * An {@link OpenemsAppInstance} is one instance of an {@link OpenemsApp} with a @@ -17,12 +19,15 @@ public class OpenemsAppInstance { public final String alias; public final UUID instanceId; public final JsonObject properties; + public final List dependencies; - public OpenemsAppInstance(String appId, String alias, UUID instanceId, JsonObject properties) { + public OpenemsAppInstance(String appId, String alias, UUID instanceId, JsonObject properties, + List dependencies) { this.appId = appId; this.alias = alias; this.instanceId = instanceId; this.properties = properties; + this.dependencies = dependencies; } @Override @@ -50,9 +55,11 @@ public int hashCode() { public JsonObject toJsonObject() { return JsonUtils.buildJsonObject() // .addProperty("appId", this.appId) // - .addProperty("alias", this.alias) // + .addProperty("alias", this.alias != null ? this.alias : "") // .addProperty("instanceId", this.instanceId.toString()) // - .add("properties", this.properties) // + .add("properties", this.properties) // TODO define if the field is editable + .onlyIf(this.dependencies != null && !this.dependencies.isEmpty(), j -> j.add("dependencies", // + this.dependencies.stream().map(Dependency::toJsonObject).collect(JsonUtils.toJsonArray()))) .build(); } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/TranslationUtil.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/TranslationUtil.java new file mode 100644 index 00000000000..a1a2f128088 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/TranslationUtil.java @@ -0,0 +1,28 @@ +package io.openems.edge.core.appmanager; + +import java.text.MessageFormat; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +public class TranslationUtil { + + /** + * Gets the value for the given key from the translationBundle. + * + * @param translationBundle the translation bundle + * @param key the key of the translation + * @param params the parameter of the translation + * @return the translated string or the key if the translation was not found or + * the format is invalid + */ + public static String getTranslation(ResourceBundle translationBundle, String key, Object... params) { + try { + var string = translationBundle.getString(key); + return MessageFormat.format(string, params); + } catch (MissingResourceException | IllegalArgumentException e) { + e.printStackTrace(); + return key; + } + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AggregateTask.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AggregateTask.java new file mode 100644 index 00000000000..eafd5ae6ece --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AggregateTask.java @@ -0,0 +1,70 @@ +package io.openems.edge.core.appmanager.dependency; + +import java.util.List; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.EdgeConfig; +import io.openems.edge.common.user.User; +import io.openems.edge.core.appmanager.AppConfiguration; + +public interface AggregateTask { + + /** + * Aggregates the given instance. + * + * @param instance the {@link AppConfiguration} of the instance + * @param oldConfig the old configuration of the instance + */ + public void aggregate(AppConfiguration instance, AppConfiguration oldConfig); + + /** + * e. g. creates components that were aggregated by the instances and my also + * delete unused components. + * + * @param user the executing user + * @param otherAppConfigurations the other existing {@link AppConfiguration}s + * @throws OpenemsNamedException on error + */ + public void create(User user, List otherAppConfigurations) throws OpenemsNamedException; + + /** + * e. g. deletes components that were aggregated. + * + * @param user the executing user + * @param otherAppConfigurations the other existing {@link AppConfiguration}s + * @throws OpenemsNamedException on error + */ + public void delete(User user, List otherAppConfigurations) throws OpenemsNamedException; + + /** + * Resets the task. + */ + public void reset(); + + public static interface ComponentAggregateTask extends AggregateTask { + + /** + * Gets the Components that were created. + * + * @return the created {@link EdgeConfig.Component} + */ + public List getCreatedComponents(); + + /** + * Gets the Components that were deleted. + * + * @return the id's of the deleted {@link EdgeConfig.Component} + */ + public List getDeletedComponents(); + + } + + public static interface SchedulerAggregateTask extends AggregateTask { + + } + + public static interface StaticIpAggregateTask extends AggregateTask { + + } + +} 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 new file mode 100644 index 00000000000..55d591f9559 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelper.java @@ -0,0 +1,50 @@ +package io.openems.edge.core.appmanager.dependency; + +import com.google.gson.JsonObject; + +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; + +public interface AppManagerAppHelper { + + /** + * Installs an {@link OpenemsApp} with all its {@link Dependency}s. + * + * @param user the executing user + * @param properties the properties of the {@link OpenemsAppInstance} + * @param alias the alias of the {@link OpenemsAppInstance} + * @param app the {@link OpenemsApp} + * @return s a list of the created {@link OpenemsAppInstance}s + * @throws OpenemsNamedException on error + */ + public UpdateValues installApp(User user, JsonObject properties, String alias, OpenemsApp app) + throws OpenemsNamedException; + + /** + * Updates an existing {@link OpenemsAppInstance}. + * + * @param user the executing user + * @param oldInstance the old {@link OpenemsAppInstance} with its + * configurations. + * @param properties the properties of the new {@link OpenemsAppInstance} + * @param alias the alias of the new {@link OpenemsAppInstance} + * @param app the {@link OpenemsApp} + * @return s a list of the replaced {@link OpenemsAppInstance}s + * @throws OpenemsNamedException on error + */ + public UpdateValues updateApp(User user, OpenemsAppInstance oldInstance, JsonObject properties, String alias, + OpenemsApp app) throws OpenemsNamedException; + + /** + * Deletes an {@link OpenemsAppInstance}. + * + * @param user the executing user + * @param instance the instance to delete + * @return s a list of the removed {@link OpenemsAppInstance}s + * @throws OpenemsNamedException on error + */ + public UpdateValues deleteApp(User user, OpenemsAppInstance instance) throws OpenemsNamedException; + +} 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 new file mode 100644 index 00000000000..0962c2fc02f --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelperImpl.java @@ -0,0 +1,1141 @@ +package io.openems.edge.core.appmanager.dependency; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.TreeMap; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Optional; +import com.google.gson.JsonObject; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; +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.AppManager; +import io.openems.edge.core.appmanager.AppManagerImpl; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ComponentUtilImpl; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppInstance; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.dependency.DependencyDeclaration.AppDependencyConfig; +import io.openems.edge.core.appmanager.validator.Validator; + +@Component +public class AppManagerAppHelperImpl implements AppManagerAppHelper { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private final ComponentManager componentManager; + private final ComponentUtil componentUtil; + + private final Validator validator; + + // tasks + private final AggregateTask.ComponentAggregateTask componentsTask; + private final AggregateTask.SchedulerAggregateTask schedulerTask; + private final AggregateTask.StaticIpAggregateTask staticIpTask; + + private final AggregateTask[] tasks; + + // TODO maybe add temporary fields with currently installing apps + + @Activate + public AppManagerAppHelperImpl(@Reference ComponentManager componentManager, @Reference ComponentUtil componentUtil, + @Reference Validator validator, @Reference AggregateTask.ComponentAggregateTask componentsTask, + @Reference AggregateTask.SchedulerAggregateTask schedulerTask, + @Reference AggregateTask.StaticIpAggregateTask staticIpTask) { + this.componentManager = componentManager; + this.componentUtil = componentUtil; + this.validator = validator; + this.componentsTask = componentsTask; + this.schedulerTask = schedulerTask; + this.staticIpTask = staticIpTask; + this.tasks = new AggregateTask[] { componentsTask, schedulerTask, staticIpTask }; + } + + @Override + public UpdateValues installApp(User user, JsonObject properties, String alias, OpenemsApp app) + throws OpenemsNamedException { + return this.updateApp(user, null, properties, alias, app); + } + + @Override + public UpdateValues updateApp(User user, OpenemsAppInstance oldInstance, JsonObject properties, String alias, + OpenemsApp app) throws OpenemsNamedException { + this.resetTasks(); + // TODO maybe check for all apps + // if also checking dependencies these may be inconsistent + // e. g. install HOME is requested it may have a dependency on a SOCOMEC Meter + // but the meter has a checkable that there has to be a HOME installed + // maybe add temporary apps in this component + final var warnings = new LinkedList(); + final var language = user == null ? null : user.getLanguage(); + final var bundle = getTranslationBundle(language); + if (oldInstance == null) { + this.checkStatus(app, language); + } else { + // determine if properties are allowed to be updated + var references = this.getAppsWithReferenceTo(oldInstance.instanceId); + for (var entry : this.getAppManagerImpl().appConfigs(references, null)) { + for (var dependencieDeclaration : entry.getValue().dependencies) { + + var dd = entry.getKey().dependencies.stream() + .filter(d -> d.instanceId.equals(oldInstance.instanceId)) + .filter(d -> d.key.equals(dependencieDeclaration.key)).findAny(); + + if (dd.isEmpty()) { + continue; + } + + var dependencyApp = this.getAppManagerImpl().findInstanceById(dd.get().instanceId); + + var appConfig = this.getAppDependencyConfig(dependencyApp, dependencieDeclaration.appConfigs); + + if (appConfig == null) { + continue; + } + + switch (dependencieDeclaration.dependencyUpdatePolicy) { + case ALLOW_ALL: + // everything can be changed + break; + case ALLOW_NONE: + throw new OpenemsException(TranslationUtil.getTranslation(bundle, "appNotAllowedToBeUpdated")); + case ALLOW_ONLY_UNCONFIGURED_PROPERTIES: + // override properties + for (var propEntry : appConfig.properties.entrySet()) { + if (!properties.has(propEntry.getKey()) + || !properties.get(propEntry.getKey()).equals(propEntry.getValue())) { + + warnings.add(TranslationUtil.getTranslation(bundle, "canNotChangeProperty", + propEntry.getKey())); + + properties.add(propEntry.getKey(), propEntry.getValue()); + } + + } + // override alias if set + if (appConfig.alias != null && !alias.equals(appConfig.alias)) { + warnings.add(TranslationUtil.getTranslation(bundle, "canNotChangeAlias")); + alias = appConfig.alias; + } + break; + } + } + } + } + + var errors = new LinkedList(); + + var oldInstances = new TreeMap(); + var dependencieInstances = new HashMap(); + // get all existing app dependencies + if (oldInstance != null) { + this.foreachExistingDependency(oldInstance, ConfigurationTarget.UPDATE, language, dc -> { + if (!dc.isDependency()) { + return true; + } + oldInstances.put(new AppIdKey(dc.parentInstance.appId, dc.sub.key), dc); + return true; + }); + } + + BiFunction includeDependency = (a, d) -> { + var oldAppConfig = oldInstances.get(new AppIdKey(a.getAppId(), d.key)); + + if (oldAppConfig == null) { + switch (d.createPolicy) { + case ALWAYS: + case IF_NOT_EXISTING: + return true; + case NEVER: + var possibleInstance = this.findNeededApp(d, this.determineDependencyConfig(d.appConfigs)); + if (possibleInstance != null) { + return true; + } + return false; + } + } + return true; + }; + + var modifiedOrCreatedApps = new ArrayList(); + var deletedApps = new ArrayList(); + final var lastCreatedOrModifiedApp = new MutableValue(); + // update app and its dependencies + this.foreachDependency(app, alias, properties, ConfigurationTarget.UPDATE, language, + this::determineDependencyConfig, includeDependency, dc -> { + // get old instance if existing + ExistingDependencyConfig oldAppConfig = null; + if (oldInstance != null) { + if (dc.isDependency()) { + oldAppConfig = oldInstances.remove(new AppIdKey(dc.parent.getAppId(), dc.sub.key)); + if (oldAppConfig != null) { + for (var entry : oldAppConfig.appDependencyConfig.properties.entrySet()) { + // add old values which are not set by the DependecyDeclaration + if (!dc.appDependencyConfig.properties.has(entry.getKey())) { + dc.appDependencyConfig.properties.add(entry.getKey(), entry.getValue()); + } + } + + } + } else { + AppConfiguration oldAppConfiguration = null; + try { + oldAppConfiguration = dc.app.getAppConfiguration(ConfigurationTarget.UPDATE, + oldInstance.properties, language); + + } catch (OpenemsNamedException e) { + this.log.error(e.getMessage()); + errors.add(TranslationUtil.getTranslation(bundle, "canNotGetAppConfiguration")); + } + + var appDependencyConfig = DependencyDeclaration.AppDependencyConfig.create() // + .setAppId(app.getAppId()) // + .setAlias(oldInstance.alias) // + .setProperties(oldInstance.properties) // + .build(); + oldAppConfig = new ExistingDependencyConfig(app, null, null, oldAppConfiguration, + appDependencyConfig, null, null, oldInstance); + } + } + + // map dependencies if this is the parent + var dependecies = new ArrayList(dependencieInstances.size()); + if (!dependencieInstances.isEmpty()) { + var isParent = !dc.isDependency(); + for (var dependency : dependencieInstances.entrySet()) { + if (!isParent && !dc.config.dependencies.stream() + .anyMatch(t -> t.equals(dependency.getKey().sub))) { + isParent = false; + break; + } + isParent = true; + dependecies + .add(new Dependency(dependency.getKey().sub.key, dependency.getValue().instanceId)); + } + if (isParent) { + dependencieInstances.clear(); + } + } + + var otherAppConfigs = this.getAppManagerImpl().getOtherAppConfigurations( + modifiedOrCreatedApps.stream().map(t -> t.instanceId).toArray(UUID[]::new)); + + // create app or get as dependency + if (oldAppConfig == null) { + var neededApp = this.findNeededApp(dc.sub, dc.appDependencyConfig); + if (neededApp == null) { + return false; + } + AppConfiguration oldConfig = null; + UUID instanceId; + OpenemsAppInstance oldInstanceOfCurrentApp = null; + var aliasOfNewInstance = dc.appDependencyConfig.alias; + if (neededApp.isPresent()) { + instanceId = neededApp.get().instanceId; + oldInstanceOfCurrentApp = neededApp.get(); + if (dc.sub.updatePolicy.isAllowedToUpdate(this.getAppManagerImpl().getInstantiatedApps(), + null, neededApp.get())) { + try { + // update app + oldConfig = dc.app.getAppConfiguration(ConfigurationTarget.UPDATE, + neededApp.get().properties, language); + for (var entry : neededApp.get().properties.entrySet()) { + // add old values which are not set by the DependecyDeclaration + if (!dc.appDependencyConfig.properties.has(entry.getKey())) { + dc.appDependencyConfig.properties.add(entry.getKey(), entry.getValue()); + } + } + + if (aliasOfNewInstance == null) { + aliasOfNewInstance = oldInstanceOfCurrentApp.alias; + } + + } catch (OpenemsNamedException e) { + this.log.error(e.getMessage()); + errors.add(TranslationUtil.getTranslation(bundle, "canNotGetAppConfiguration")); + } + } + } else { + // create app + instanceId = UUID.randomUUID(); + + // use app name as default alias if not given + if (aliasOfNewInstance == null) { + aliasOfNewInstance = dc.app.getName(language); + } + + // check if the created app can satisfy another app dependency + final var fallBackAlwaysCreateApp = new MutableValue(); + + var apps2UpdateDependency = this.getAppManagerImpl().getInstantiatedApps().stream() // + .filter(i -> { + var neededDependency = this.getNeededDependencyTo(i, dc.app.getAppId()); + if (neededDependency == null) { + return false; + } + if (neededDependency.createPolicy == DependencyDeclaration.CreatePolicy.ALWAYS) { + // only set the dependency to one app which has the always create policy + fallBackAlwaysCreateApp.setValue(i); + return false; + } + return true; + }) // + .collect(Collectors.toList()); + + if (apps2UpdateDependency.isEmpty() && fallBackAlwaysCreateApp.getValue() != null) { + apps2UpdateDependency.add(fallBackAlwaysCreateApp.getValue()); + } + + for (var instance : apps2UpdateDependency) { + var neededDependency = this.getNeededDependencyTo(instance, dc.app.getAppId()); + // override properties if set by dependency + if (neededDependency.dependencyUpdatePolicy != DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ALL) { + var config = this.determineDependencyConfig(neededDependency.appConfigs); + for (var entry : config.properties.entrySet()) { + if (!dc.appDependencyConfig.properties.has(entry.getKey()) + || !dc.appDependencyConfig.properties.get(entry.getKey()) + .equals(entry.getValue())) { + warnings.add(TranslationUtil.getTranslation(bundle, "overrideProperty", + entry.getKey())); + } + dc.appDependencyConfig.properties.add(entry.getKey(), entry.getValue()); + } + } + + // update dependencies + var alreadyModifiedAppIndex = modifiedOrCreatedApps.indexOf(instance); + var replaceApp = instance; + if (alreadyModifiedAppIndex != -1) { + replaceApp = modifiedOrCreatedApps.get(alreadyModifiedAppIndex); + } + var newDependencies = new ArrayList(); + if (replaceApp.dependencies != null) { + newDependencies.addAll(replaceApp.dependencies); + } + newDependencies.add(new Dependency(neededDependency.key, instanceId)); + modifiedOrCreatedApps.remove(replaceApp); + modifiedOrCreatedApps.add(new OpenemsAppInstance(replaceApp.appId, replaceApp.alias, + replaceApp.instanceId, replaceApp.properties, newDependencies)); + } + } + + var newAppInstance = new OpenemsAppInstance(dc.app.getAppId(), aliasOfNewInstance, instanceId, + dc.appDependencyConfig.properties, dependecies); + lastCreatedOrModifiedApp.setValue(newAppInstance); + modifiedOrCreatedApps.add(newAppInstance); + dependencieInstances.put(dc, newAppInstance); + try { + var newConfig = this.getNewAppConfigWithReplacedIds(dc.app, oldInstanceOfCurrentApp, + newAppInstance, AppManagerAppHelperImpl.getComponentsFromConfigs(otherAppConfigs), + language); + + this.aggregateAllTasks(newConfig, oldConfig); + } catch (OpenemsNamedException e) { + this.log.error(e.getMessage()); + errors.add(TranslationUtil.getTranslation(bundle, "canNotGetAppConfiguration")); + } + return true; + } + + // find parent + OpenemsAppInstance parent = null; + if (dc.isDependency()) { + if (dc.parent.getAppId().equals(oldInstance.appId)) { + parent = oldInstance; + } else { + for (var entry : oldInstances.entrySet()) { + if (entry.getValue().app.equals(dc.parent)) { + parent = entry.getValue().instance; + break; + } + } + } + } + + // update existing app + var isNotAllowedToUpdate = dc.isDependency() + && !dc.sub.updatePolicy.isAllowedToUpdate(this.getAppManagerImpl().getInstantiatedApps(), + parent, oldAppConfig.instance); + + var newInstanceAlias = dc.appDependencyConfig.alias; + if (newInstanceAlias == null) { + newInstanceAlias = oldAppConfig.instance.alias; + } + + OpenemsAppInstance newAppInstance; + + if (isNotAllowedToUpdate) { + newAppInstance = oldAppConfig.instance; + } else { + newAppInstance = new OpenemsAppInstance(dc.app.getAppId(), newInstanceAlias, + oldAppConfig.instance.instanceId, dc.appDependencyConfig.properties, dependecies); + } + + lastCreatedOrModifiedApp.setValue(newAppInstance); + dependencieInstances.put(dc, newAppInstance); + + if (isNotAllowedToUpdate) { + // not allowed to update but still a dependency + return true; + } + modifiedOrCreatedApps.add(newAppInstance); + + try { + var newAppConfig = this.getNewAppConfigWithReplacedIds(dc.app, oldAppConfig.instance, + newAppInstance, AppManagerAppHelperImpl.getComponentsFromConfigs(otherAppConfigs), + language); + + this.aggregateAllTasks(newAppConfig, oldAppConfig.config); + + } catch (OpenemsNamedException e) { + this.log.error(e.getMessage()); + errors.add(TranslationUtil.getTranslation(bundle, "canNotGetAppConfiguration")); + } + + return true; + }); + + // add removed apps for deletion + for (var entry : oldInstances.entrySet()) { + var dc = entry.getValue(); + if (!dc.sub.deletePolicy.isAllowedToDelete(this.getAppManagerImpl().getInstantiatedApps(), + dc.parentInstance, dc.instance)) { + continue; + } + this.aggregateAllTasks(null, dc.config); + deletedApps.add(dc.instance); + } + + var ignoreInstances = new ArrayList<>(modifiedOrCreatedApps); + ignoreInstances + .addAll(oldInstances.entrySet().stream().map(t -> t.getValue().instance).collect(Collectors.toList())); + + var otherAppConfigs = this.getAppManagerImpl() + .getOtherAppConfigurations(ignoreInstances.stream().map(t -> t.instanceId).toArray(UUID[]::new)); + + try { + // create or delete unused components + this.componentsTask.create(user, otherAppConfigs); + } catch (OpenemsNamedException e) { + this.log.error(e.getMessage()); + errors.add(TranslationUtil.getTranslation(bundle, "canNotUpdateComponents")); + } + + try { + // update scheduler execute order + this.schedulerTask.create(user, otherAppConfigs); + } catch (OpenemsNamedException e) { + this.log.error(e.getMessage()); + errors.add(TranslationUtil.getTranslation(bundle, "canNotUpdateScheduler")); + } + + try { + // update static ips + this.staticIpTask.create(user, otherAppConfigs); + } catch (OpenemsNamedException e) { + this.log.error(e.getMessage()); + errors.add(TranslationUtil.getTranslation(bundle, "canNotUpdateStaticIps")); + } + + if (!errors.isEmpty()) { + throw new OpenemsException(errors.stream().collect(Collectors.joining("|"))); + } + + return new UpdateValues(lastCreatedOrModifiedApp.getValue(), modifiedOrCreatedApps, deletedApps, warnings); + } + + private DependencyDeclaration.AppDependencyConfig getAppDependencyConfig(OpenemsAppInstance instance, + List appDependencyConfigs) { + for (var config : appDependencyConfigs) { + if (config.appId != null && config.appId.equals(instance.appId)) { + return config; + } + if (config.specificInstanceId.equals(instance.instanceId)) { + return config; + } + } + return null; + } + + private static final class MutableValue { + + private T value; + + public MutableValue() { + this(null); + } + + public MutableValue(T value) { + this.setValue(value); + } + + public void setValue(T value) { + this.value = value; + } + + public T getValue() { + return this.value; + } + + } + + private final DependencyDeclaration getNeededDependencyTo(OpenemsAppInstance instance, String appId) { + var app = this.getAppManagerImpl().findAppById(instance.appId); + try { + var neededDependencies = app.getAppConfiguration(ConfigurationTarget.UPDATE, instance.properties, + null).dependencies; + if (neededDependencies == null || neededDependencies.isEmpty()) { + return null; + } + for (var neededDependency : neededDependencies) { + // remove already satisfied dependencies + if (instance.dependencies != null + && instance.dependencies.stream().anyMatch(d -> d.key.equals(neededDependency.key))) { + continue; + } + // TODO when adding an app the current app can't be referenced + if (neededDependency.appConfigs.stream().filter(c -> c.appId != null) + .anyMatch(c -> c.appId.equals(appId))) { + return neededDependency; + } + if (neededDependency.appConfigs.stream().filter(c -> c.specificInstanceId != null) + .anyMatch(c -> c.specificInstanceId.equals(instance.instanceId))) { + return neededDependency; + } + + } + } catch (OpenemsNamedException e) { + // can not get app configuration + } + return null; + } + + private static class AppIdKey implements Comparable { + public final String appId; + public final String key; + + public AppIdKey(String appId, String key) { + this.appId = appId; + this.key = key; + } + + @Override + public int compareTo(AppIdKey o) { + return this.toString().compareTo(o.toString()); + } + + @Override + public String toString() { + return this.appId + ":" + this.key; + } + } + + @Override + public UpdateValues deleteApp(User user, OpenemsAppInstance instance) throws OpenemsNamedException { + this.resetTasks(); + + final var language = user == null ? null : user.getLanguage(); + final var bundle = getTranslationBundle(language); + // check if the app is allowed to be delete + if (!this.isAllowedToDelete(instance)) { + throw new OpenemsException(TranslationUtil.getTranslation(bundle, "appNotAllowedToBeDeleted")); + } + + var deletedInstances = new LinkedList(); + final var errors = new LinkedList(); + + this.foreachExistingDependency(instance, ConfigurationTarget.DELETE, language, dc -> { + + // check if dependency is allowed to be deleted by its parent + if (dc.isDependency()) { + switch (dc.sub.deletePolicy) { + case NEVER: + return false; + case IF_MINE: + if (this.getAppManagerImpl().getInstantiatedApps().stream() + .anyMatch(a -> !a.equals(dc.parentInstance) && a.dependencies != null && a.dependencies + .stream().anyMatch(d -> d.instanceId.equals(dc.instance.instanceId)))) { + return false; + } + break; + case ALWAYS: + break; + } + } + + deletedInstances.add(dc.instance); + + this.aggregateAllTasks(null, dc.config); + + return true; + }); + + var unmodifiedApps = this + .getAppsWithReferenceTo(deletedInstances.stream().map(t -> t.instanceId).toArray(UUID[]::new)).stream() + .filter(a -> !deletedInstances.stream().anyMatch(t -> t.equals(a))).collect(Collectors.toList()); + + var modifiedApps = new ArrayList(unmodifiedApps.size()); + for (var app : unmodifiedApps) { + var dependencies = new ArrayList<>(app.dependencies); + dependencies.removeIf(d -> deletedInstances.stream().anyMatch(i -> i.instanceId.equals(d.instanceId))); + modifiedApps.add(new OpenemsAppInstance(app.appId, // + app.alias, app.instanceId, app.properties, dependencies)); + } + + var otherAppConfigs = this.getAppManagerImpl() + .getOtherAppConfigurations(deletedInstances.stream().map(t -> t.instanceId).toArray(UUID[]::new)); + + try { + // delete components + this.componentsTask.delete(user, otherAppConfigs); + } catch (OpenemsNamedException e) { + this.log.error(e.getMessage()); + errors.add(TranslationUtil.getTranslation(bundle, "canNotUpdateComponents")); + } + + try { + // remove ids in scheduler + this.schedulerTask.delete(user, otherAppConfigs); + } catch (OpenemsNamedException e) { + this.log.error(e.getMessage()); + errors.add(TranslationUtil.getTranslation(bundle, "canNotUpdateScheduler")); + } + + try { + // remove static ips + this.staticIpTask.delete(user, otherAppConfigs); + } catch (OpenemsNamedException e) { + this.log.error(e.getMessage()); + errors.add(TranslationUtil.getTranslation(bundle, "canNotUpdateStaticIps")); + } + + if (!errors.isEmpty()) { + throw new OpenemsException(errors.stream().collect(Collectors.joining("|"))); + } + + return new UpdateValues(instance, modifiedApps, deletedInstances); + } + + private List getAppsWithReferenceTo(UUID... instanceIds) { + return this.getAppManagerImpl().getInstantiatedApps() // + .stream() // + .filter(i -> i.dependencies != null && !i.dependencies.isEmpty()) // + .filter(i -> i.dependencies.stream().anyMatch(// + d -> Arrays.stream(instanceIds).anyMatch(id -> id.equals(d.instanceId)))) // + .collect(Collectors.toList()); + } + + private final void aggregateAllTasks(AppConfiguration instance, AppConfiguration oldInstance) { + for (var task : this.tasks) { + task.aggregate(instance, oldInstance); + } + } + + private void resetTasks() { + for (var task : this.tasks) { + task.reset(); + } + } + + /** + * Checks if the instance is allowed to be deleted depending on other apps + * dependencies to this instance. + * + * @param instance the app to delete + * @param ignoreIds the instance id's that should be ignored + * @return true if it is allowed to delete the app + */ + private final boolean isAllowedToDelete(OpenemsAppInstance instance, UUID... ignoreIds) { + // check if a parent does not allow deletion of this instance + for (var entry : this.getAppManagerImpl().appConfigs(this.getAppsWithReferenceTo(instance.instanceId), + AppManagerImpl.exludingInstanceIds(ignoreIds))) { + for (var dependency : entry.getKey().dependencies) { + if (!dependency.instanceId.equals(instance.instanceId)) { + continue; + } + var declaration = entry.getValue().dependencies.stream().filter(dd -> dd.key.equals(dependency.key)) + .findAny(); + + // declaration not found for dependency + if (declaration.isEmpty()) { + continue; + } + + switch (declaration.get().dependencyDeletePolicy) { + case ALLOWED: + break; + case NOT_ALLOWED: + return false; + } + } + } + return true; + } + + protected void checkStatus(OpenemsApp openemsApp, Language language) throws OpenemsNamedException { + var validatorConfig = openemsApp.getValidatorConfig(); + var status = this.validator.getStatus(validatorConfig); + switch (status) { + case INCOMPATIBLE: + throw new OpenemsException("App is not compatible! " + this.validator + .getErrorCompatibleMessages(validatorConfig, language).stream().collect(Collectors.joining(";"))); + case COMPATIBLE: + throw new OpenemsException("App can not be installed! " + this.validator + .getErrorInstallableMessages(validatorConfig, language).stream().collect(Collectors.joining(";"))); + case INSTALLABLE: + // app can be installed + return; + } + throw new OpenemsException("Status '" + status.name() + "' is not implemented."); + } + + protected static List getComponentsFromConfigs(List configs) { + var components = new LinkedList(); + for (var config : configs) { + components.addAll(config.components); + } + return components; + } + + protected static List getSchedulerIdsFromConfigs(List configs) { + var ids = new LinkedList(); + for (var config : configs) { + ids.addAll(config.schedulerExecutionOrder); + } + return ids; + } + + protected static List getStaticIpsFromConfigs(List configs) { + var ips = new LinkedList(); + for (var config : configs) { + ips.addAll(config.ips); + } + return ips; + } + + /** + * Finds the needed app for a {@link DependencyDeclaration}. + * + * @param declaration the current {@link DependencyConfig} + * @param config the current + * {@link DependencyDeclaration.AppDependencyConfig} + * @return s null if the app can not be added; {@link Optional#absent()} if the + * app needs to be created; the {@link OpenemsAppInstance} if an + * existing app can be used + */ + private Optional findNeededApp(DependencyDeclaration declaration, + DependencyDeclaration.AppDependencyConfig config) { + if (declaration == null) { + return Optional.absent(); + } + if (config.specificInstanceId != null) { + try { + var appById = this.getAppManagerImpl().findInstanceById(config.specificInstanceId); + return Optional.of(appById); + } catch (NoSuchElementException e) { + return null; + } + } + var appId = config.appId; + if (declaration.createPolicy == DependencyDeclaration.CreatePolicy.ALWAYS) { + var neededApps = this.getAppManagerImpl().getInstantiatedApps().stream().filter(t -> t.appId.equals(appId)) + .collect(Collectors.toList()); + OpenemsAppInstance availableApp = null; + for (var neededApp : neededApps) { + if (this.getAppsWithDependencyTo(neededApp).isEmpty()) { + availableApp = neededApp; + break; + } + } + return Optional.fromNullable(availableApp); + } + var neededApp = this.getAppManagerImpl().getInstantiatedApps().stream().filter(t -> t.appId.equals(appId)) + .collect(Collectors.toList()); + if (!neededApp.isEmpty()) { + return Optional.of(neededApp.get(0)); + } + if (declaration.createPolicy == DependencyDeclaration.CreatePolicy.IF_NOT_EXISTING) { + return Optional.absent(); + } + return null; + } + + private List getAppsWithDependencyTo(OpenemsAppInstance instance) { + return this.getAppManagerImpl().getInstantiatedApps().stream() + .filter(t -> t.dependencies != null && !t.dependencies.isEmpty()) + .filter(t -> t.dependencies.stream().anyMatch(d -> d.instanceId.equals(instance.instanceId))) + .collect(Collectors.toList()); + } + + /** + * Recursively iterates over all dependencies and the given app. + * + *

+ * Order bottom -> top. + * + * @param app the app to be installed + * @param appConfig the {@link AppDependencyConfig} of the + * current app + * @param target the {@link ConfigurationTarget} + * @param function returns true if the instance gets created or + * already exists + * @param sub the {@link DependencyDeclaration} + * @param l the {@link Language} + * @param parent the parent app + * @param alreadyIteratedApps the apps that already got iterated thru to + * avoid endless loop. e. g. if two apps have + * each other as a dependency + * @param determineDependencyConfig the function to determine the + * {@link AppDependencyConfig} + * @param includeDependency a {@link BiFunction} to determine if a + * dependency should get included + * @return s the last {@link DependencyConfig} + * @throws OpenemsNamedException on error + */ + private DependencyConfig foreachDependency(OpenemsApp app, AppDependencyConfig appConfig, + ConfigurationTarget target, Function function, DependencyDeclaration sub, + Language l, OpenemsApp parent, Set alreadyIteratedApps, + Function, AppDependencyConfig> determineDependencyConfig, + BiFunction includeDependency) throws OpenemsNamedException { + if (alreadyIteratedApps == null) { + alreadyIteratedApps = new HashSet<>(); + } + alreadyIteratedApps.add(app); + if (appConfig.alias != null) { + appConfig.properties.addProperty("ALIAS", appConfig.alias); + } + var config = app.getAppConfiguration(target, appConfig.properties, l); + if (appConfig.alias != null) { + appConfig.properties.remove("ALIAS"); + } + var dependencies = new LinkedList(); + for (var dependency : config.dependencies) { + var nextAppConfig = determineDependencyConfig.apply(dependency.appConfigs); + if (nextAppConfig == null) { + // can not determine one out of many configs + continue; + } + try { + OpenemsApp dependencyApp; + if (nextAppConfig.appId != null) { + dependencyApp = this.getAppManagerImpl().findAppById(nextAppConfig.appId); + } else { + var specificApp = this.getAppManagerImpl().findInstanceById(nextAppConfig.specificInstanceId); + dependencyApp = this.getAppManagerImpl().findAppById(specificApp.appId); + } + if (alreadyIteratedApps.contains(dependencyApp)) { + continue; + } + if (!includeDependency.apply(app, dependency)) { + continue; + } + + var addingConfig = this.foreachDependency(dependencyApp, nextAppConfig, target, function, dependency, l, + app, alreadyIteratedApps, determineDependencyConfig, includeDependency); + if (addingConfig != null) { + dependencies.add(addingConfig); + } + } catch (NoSuchElementException e) { + // can not find app + e.printStackTrace(); + } + } + + var newConfig = new DependencyConfig(app, parent, sub, config, appConfig, dependencies); + if (function.apply(newConfig)) { + return newConfig; + } + return null; + } + + private void foreachDependency(OpenemsApp app, String alias, JsonObject defaultProperties, + ConfigurationTarget target, Language l, + Function, AppDependencyConfig> determineDependencyConfig, + BiFunction includeDependency, + Function consumer) throws OpenemsNamedException { + var appConfig = DependencyDeclaration.AppDependencyConfig.create() // + .setAppId(app.getAppId()) // + .setAlias(alias) // + .setProperties(defaultProperties) // + .build(); + this.foreachDependency(app, appConfig, target, consumer, null, l, null, null, determineDependencyConfig, + includeDependency); + } + + private DependencyDeclaration.AppDependencyConfig determineDependencyConfig(List configs) { + if (configs == null || configs.isEmpty()) { + return null; + } + if (configs.size() == 1) { + return configs.get(0); + } + + for (var config : configs) { + var instances = this.getAppManagerImpl().getInstantiatedApps().stream() + .filter(i -> i.appId.equals(config.appId)).collect(Collectors.toList()); + for (var instance : instances) { + var existingDependencies = this.getAppsWithDependencyTo(instance); + if (existingDependencies.isEmpty()) { + return config; + } + } + } + + return configs.get(0); + } + + private void foreachExistingDependency(OpenemsAppInstance instance, ConfigurationTarget target, Language l, + Function consumer) throws OpenemsNamedException { + this.foreachExistingDependency(instance, target, consumer, null, null, l, null); + } + + /** + * Recursively iterates over all existing dependencies and the given app. + * + *

+ * Order bottom -> top. + * + * @param instance the existing {@link OpenemsAppInstance} + * @param target the {@link ConfigurationTarget} + * @param consumer the consumer that gets executed for every instance + * @param parent the parent instance of the current dependency + * @param sub the {@link DependencyDeclaration} + * @param l the {@link Language} + * @param alreadyIteratedApps the already iterated app to avoid an endless loop + * @return s the last {@link DependencyConfig} + * @throws OpenemsNamedException on error + */ + private DependencyConfig foreachExistingDependency(OpenemsAppInstance instance, ConfigurationTarget target, + Function consumer, OpenemsAppInstance parent, DependencyDeclaration sub, + Language l, Set alreadyIteratedApps) throws OpenemsNamedException { + if (alreadyIteratedApps == null) { + alreadyIteratedApps = new HashSet<>(); + } + alreadyIteratedApps.add(instance); + var app = this.getAppManagerImpl().findAppById(instance.appId); + instance.properties.addProperty("ALIAS", instance.alias); + var config = app.getAppConfiguration(target, instance.properties, l); + instance.properties.remove("ALIAS"); + + var dependecies = new ArrayList(); + if (instance.dependencies != null) { + dependecies = new ArrayList<>(instance.dependencies.size()); + for (var dependency : instance.dependencies) { + try { + var dependencyApp = this.getAppManagerImpl().findInstanceById(dependency.instanceId); + if (alreadyIteratedApps.contains(dependencyApp)) { + continue; + } + var subApp = config.dependencies.stream().filter(t -> t.key.equals(dependency.key)).findFirst() + .get(); + var dependecy = this.foreachExistingDependency(dependencyApp, target, consumer, instance, subApp, l, + alreadyIteratedApps); + dependecies.add(dependecy); + } catch (NoSuchElementException e) { + // can not find app + } + } + } + OpenemsApp parentApp = null; + if (parent != null) { + parentApp = this.getAppManagerImpl().findAppById(parent.appId); + } + + DependencyDeclaration.AppDependencyConfig dependencyAppConfig; + if (sub == null) { + dependencyAppConfig = DependencyDeclaration.AppDependencyConfig.create() // + .setAppId(instance.appId) // + .setProperties(instance.properties) // + .setAlias(instance.alias) // + .build(); + } else { + dependencyAppConfig = this.getAppDependencyConfig(instance, sub.appConfigs); + } + + var newConfig = new ExistingDependencyConfig(app, parentApp, sub, config, dependencyAppConfig, dependecies, + parent, instance); + if (consumer.apply(newConfig)) { + return newConfig; + } + return null; + } + + /** + * Gets the component id s that can be replaced. + * + * @param app the components of which app + * @param properties the default properties to create an app instance of this + * app + * @return a map of the component id s that can be replaced mapped from id to + * key to put the next id + * @throws OpenemsNamedException on error + */ + protected final Map getReplaceableComponentIds(OpenemsApp app, JsonObject properties) + throws OpenemsNamedException { + final var prefix = "?_?_"; + var config = app.getAppConfiguration(ConfigurationTarget.TEST, properties, null); + var copyBuilder = JsonUtils.buildJsonObject(); + for (var entry : properties.entrySet()) { + copyBuilder.add(entry.getKey(), entry.getValue()); + } + for (var comp : config.components) { + copyBuilder.addProperty(comp.getId(), prefix); + } + var copy = copyBuilder.build(); + var configWithNewIds = app.getAppConfiguration(ConfigurationTarget.TEST, copy, null); + Map replaceableComponentIds = new HashMap<>(); + for (var comp : configWithNewIds.components) { + if (comp.getId().startsWith(prefix)) { + // "METER_ID:meter0" + var raw = comp.getId().substring(prefix.length()); + // ["METER_ID", "meter0"] + var pieces = raw.split(":"); + // "METER_ID" + var property = pieces[0]; + // "meter0" + var defaultId = pieces[1]; + replaceableComponentIds.put(defaultId, property); + } + } + return replaceableComponentIds; + } + + /** + * Gets an App Configuration with component id s, which can be used to create or + * rewrite the settings of the component. + * + * @param app the {@link OpenemsApp} + * @param oldAppInstance the old {@link OpenemsAppInstance} + * @param newAppInstance the new {@link OpenemsAppInstance} + * @param otherAppComponents the components that are used from the other + * {@link OpenemsAppInstance} + * @param language the language of the new config + * @return the AppConfiguration with the replaced ID s of the components + * @throws OpenemsNamedException on error + */ + private AppConfiguration getNewAppConfigWithReplacedIds(OpenemsApp app, OpenemsAppInstance oldAppInstance, + OpenemsAppInstance newAppInstance, List otherAppComponents, Language language) + throws OpenemsNamedException { + + var target = oldAppInstance == null ? ConfigurationTarget.ADD : ConfigurationTarget.UPDATE; + var newAppConfig = app.getAppConfiguration(target, newAppInstance.properties, language); + + final var replacableIds = this.getReplaceableComponentIds(app, newAppInstance.properties); + + for (var comp : ComponentUtilImpl.order(newAppConfig.components)) { + // replace old id s with new ones + for (var entry : comp.getProperties().entrySet()) { + for (var replaceableId : replacableIds.entrySet()) { + if (entry.getValue().toString().contains(replaceableId.getKey())) { + var newId = entry.getValue().toString().replace(replaceableId.getKey(), + newAppInstance.properties.get(replaceableId.getValue()).getAsString()); + newId = newId.replace("\"", ""); + var newValue = JsonUtils.getAsJsonElement(newId); + comp.getProperties().put(entry.getKey(), newValue); + } + } + } + + var isNewComponent = true; + var id = comp.getId(); + var canBeReplaced = replacableIds.containsKey(id); + EdgeConfig.Component foundComponent = null; + + // try to find a component with the necessary settings + if (canBeReplaced) { + foundComponent = this.componentUtil.getComponentByConfig(comp); + if (foundComponent != null) { + id = foundComponent.getId(); + } + } + if (foundComponent == null && oldAppInstance != null && oldAppInstance.properties.has(id.toUpperCase())) { + id = oldAppInstance.properties.get(id.toUpperCase()).getAsString(); + foundComponent = this.componentManager.getEdgeConfig().getComponent(id).orElse(null); + final var tempId = id; + // other app uses the same component because they had the same configuration + // now this app needs the component with a different configuration so now create + // a new component + if (foundComponent != null && otherAppComponents.stream().anyMatch(t -> t.getId().equals(tempId))) { + foundComponent = null; + } + } + isNewComponent = isNewComponent && foundComponent == null; + if (isNewComponent) { + // if the id is not already set and there is no component with the default id + // then use the default id + foundComponent = this.componentManager.getEdgeConfig().getComponent(comp.getId()).orElse(null); + if (foundComponent == null) { + id = comp.getId(); + } else { + // replace number at the end and get the next available id + var nextAvailableId = this.componentUtil.getNextAvailableId(id.replaceAll("\\d+", ""), + otherAppComponents); + if (!nextAvailableId.equals(id) && !canBeReplaced) { + // component can not be created because the id is already used + // and the id can not be set in the configuration + continue; + } + if (canBeReplaced) { + id = nextAvailableId; + } + } + } + + if (canBeReplaced) { + newAppInstance.properties.addProperty(replacableIds.get(comp.getId()), id); + } + } + return app.getAppConfiguration(target, newAppInstance.properties, language); + } + + private final AppManagerImpl getAppManagerImpl() { + return (AppManagerImpl) this.componentManager.getEnabledComponentsOfType(AppManager.class).get(0); + } + + private static ResourceBundle getTranslationBundle(Language language) { + if (language == null) { + language = Language.DEFAULT; + } + // TODO translation + switch (language) { + case CZ: + case ES: + case FR: + case NL: + language = Language.EN; + break; + case DE: + case EN: + break; + } + + return ResourceBundle.getBundle("io.openems.edge.core.appmanager.dependency.translation", language.getLocal()); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/ComponentAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/ComponentAggregateTaskImpl.java new file mode 100644 index 00000000000..773911f56ea --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/ComponentAggregateTaskImpl.java @@ -0,0 +1,217 @@ +package io.openems.edge.core.appmanager.dependency; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.jsonrpc.request.CreateComponentConfigRequest; +import io.openems.common.jsonrpc.request.DeleteComponentConfigRequest; +import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest; +import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest.Property; +import io.openems.common.types.EdgeConfig; +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.componentmanager.ComponentManagerImpl; + +@Component +public class ComponentAggregateTaskImpl implements AggregateTask, AggregateTask.ComponentAggregateTask { + + private final ComponentManager componentManager; + + private List components; + private List components2Delete; + + private List createdComponents; + private List deletedComponents; + + @Activate + public ComponentAggregateTaskImpl(@Reference ComponentManager componentManager) { + this.componentManager = componentManager; + } + + @Override + public void reset() { + this.components = new LinkedList<>(); + this.components2Delete = new LinkedList<>(); + } + + @Override + public void aggregate(AppConfiguration config, AppConfiguration oldConfig) { + if (config != null) { + this.components.addAll(config.components); + } + if (oldConfig != null) { + var componentDiff = new ArrayList<>(oldConfig.components); + if (config != null) { + componentDiff.removeIf(t -> config.components.stream().anyMatch(c -> c.getId().equals(t.getId()))); + } + this.components2Delete.addAll(componentDiff); + } + } + + @Override + public void create(User user, List otherAppConfigurations) throws OpenemsNamedException { + this.createdComponents = new ArrayList<>(this.components.size()); + var errors = new LinkedList(); + var otherAppComponents = AppManagerAppHelperImpl.getComponentsFromConfigs(otherAppConfigurations); + // create components + for (var comp : ComponentUtilImpl.order(this.components)) { + /** + * if comp already exists with same config as needed => use it. if comp exist + * with different config and no other app needs it => rewrite settings. if comp + * exist with different config and other app needs it => create new comp + */ + var foundComponentWithSameId = this.componentManager.getEdgeConfig().getComponent(comp.getId()) + .orElse(null); + if (foundComponentWithSameId != null) { + + var isSameConfigWithoutAlias = ComponentUtilImpl.isSameConfigurationWithoutAlias(null, comp, + foundComponentWithSameId); + if (isSameConfigWithoutAlias && comp.getAlias() == null) { + // alias == null => no update + continue; + } + var isSameConfig = isSameConfigWithoutAlias + && comp.getAlias().equals(foundComponentWithSameId.getAlias()); + + if (isSameConfig) { + // same configuration so no reconfiguration needed + continue; + } + + // check if it is my component + if (otherAppComponents.stream().anyMatch(t -> t.getId().equals(foundComponentWithSameId.getId()))) { + // not my component but only the alias changed + if (isSameConfigWithoutAlias) { + // TODO maybe warning if the alias can't be set + continue; + } + errors.add("Configuration of component with id '" + foundComponentWithSameId.getId() + + "' can not be rewritten. Because the component belongs to another app."); + continue; + } + try { + this.reconfigure(user, comp, foundComponentWithSameId); + } catch (OpenemsNamedException e) { + errors.add(e.getMessage()); + } + continue; + } + + // create new component + try { + this.createComponent(user, comp); + this.createdComponents.add(comp); + } catch (OpenemsNamedException e) { + var error = "Component[" + comp.getFactoryId() + "] cant be created!"; + errors.add(error); + errors.add(e.getMessage()); + } + + } + try { + // delete components that were used from the old configurations + this.delete(user, otherAppConfigurations); + } catch (OpenemsNamedException e) { + errors.add(e.getMessage()); + } + + if (!errors.isEmpty()) { + throw new OpenemsException(errors.stream().collect(Collectors.joining("|"))); + } + + } + + /** + * deletes the given components only if they are not in notMyComponents. + * + * @param user the executing user + * @param otherAppConfigurations the other {@link AppConfiguration}s + */ + @Override + public void delete(User user, List otherAppConfigurations) throws OpenemsNamedException { + this.deletedComponents = new ArrayList<>(this.components2Delete.size()); + List errors = new ArrayList<>(); + var notMyComponents = AppManagerAppHelperImpl.getComponentsFromConfigs(otherAppConfigurations); + for (var comp : this.components2Delete) { + if (notMyComponents.stream().anyMatch(t -> t.getId().equals(comp.getId()))) { + continue; + } + var component = this.componentManager.getEdgeConfig().getComponent(comp.getId()).orElse(null); + if (component == null) { + // component does not exist + continue; + } + + try { + // user can be null using internal method + ((ComponentManagerImpl) this.componentManager).handleDeleteComponentConfigRequest(user, + new DeleteComponentConfigRequest(comp.getId())); + this.deletedComponents.add(comp.getId()); + } catch (OpenemsNamedException e) { + errors.add(e.toString()); + } + } + + if (!errors.isEmpty()) { + throw new OpenemsException(errors.stream().collect(Collectors.joining("|"))); + } + } + + private void createComponent(User user, EdgeConfig.Component comp) throws OpenemsNamedException { + List properties = comp.getProperties().entrySet().stream() + .map(t -> new Property(t.getKey(), t.getValue())).collect(Collectors.toList()); + properties.add(new Property("id", comp.getId())); + properties.add(new Property("alias", comp.getAlias())); + + // user can be null using internal method + ((ComponentManagerImpl) this.componentManager).handleCreateComponentConfigRequest(user, + new CreateComponentConfigRequest(comp.getFactoryId(), properties)); + } + + /** + * checks if the settings of the component changed if there is a change it + * rewrites the settings of the given component. + * + * @param user the executing user + * @param myComp the component that configuration should be rewritten + * @param actualComp the actual component that exists + * @throws OpenemsNamedException when the configuration can not be rewritten + */ + private void reconfigure(User user, EdgeConfig.Component myComp, EdgeConfig.Component actualComp) + throws OpenemsNamedException { + if (ComponentUtilImpl.isSameConfiguration(null, myComp, actualComp)) { + return; + } + + // send update request + List properties = myComp.getProperties().entrySet().stream() + .map(t -> new Property(t.getKey(), t.getValue())) // + .collect(Collectors.toList()); + properties.add(new Property("alias", myComp.getAlias())); + var updateRequest = new UpdateComponentConfigRequest(actualComp.getId(), properties); + // user can be null using internal method + ((ComponentManagerImpl) this.componentManager).handleUpdateComponentConfigRequest(user, updateRequest); + } + + @Override + public List getCreatedComponents() { + return Collections.unmodifiableList(this.createdComponents); + } + + @Override + public List getDeletedComponents() { + return Collections.unmodifiableList(this.deletedComponents); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/Dependency.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/Dependency.java new file mode 100644 index 00000000000..71ed57c8aff --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/Dependency.java @@ -0,0 +1,38 @@ +package io.openems.edge.core.appmanager.dependency; + +import java.util.UUID; + +import com.google.gson.JsonObject; + +import io.openems.common.utils.JsonUtils; +import io.openems.edge.core.appmanager.AppManager; + +/** + * Represents a dependency in the configuration of the {@link AppManager} of an + * app. + * + */ +public class Dependency { + + public final String key; + + public final UUID instanceId; + + public Dependency(String key, UUID instanceId) { + this.key = key; + this.instanceId = instanceId; + } + + /** + * Gets the {@link Dependency} as a {@link JsonObject}. + * + * @return the {@link JsonObject} + */ + public JsonObject toJsonObject() { + return JsonUtils.buildJsonObject() // + .addProperty("key", this.key) // + .addProperty("instanceId", this.instanceId.toString()) // + .build(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/DependencyConfig.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/DependencyConfig.java new file mode 100644 index 00000000000..800886edc38 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/DependencyConfig.java @@ -0,0 +1,36 @@ +package io.openems.edge.core.appmanager.dependency; + +import java.util.List; + +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.OpenemsApp; + +public class DependencyConfig { + + public final OpenemsApp app; + public final OpenemsApp parent; + + // @Nullable + // if not a dependency of an app. + public final DependencyDeclaration sub; + public final AppConfiguration config; + + public final DependencyDeclaration.AppDependencyConfig appDependencyConfig; + + public final List declarations; + + public DependencyConfig(OpenemsApp app, OpenemsApp parent, DependencyDeclaration sub, AppConfiguration config, + DependencyDeclaration.AppDependencyConfig appDependencyConfig, List declarations) { + this.app = app; + this.parent = parent; + this.sub = sub; + this.config = config; + this.appDependencyConfig = appDependencyConfig; + this.declarations = declarations; + } + + public final boolean isDependency() { + return this.sub != null; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/DependencyDeclaration.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/DependencyDeclaration.java new file mode 100644 index 00000000000..c2588d970e0 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/DependencyDeclaration.java @@ -0,0 +1,246 @@ +package io.openems.edge.core.appmanager.dependency; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.function.BiFunction; + +import com.google.common.base.Function; +import com.google.gson.JsonObject; + +import io.openems.edge.core.appmanager.OpenemsAppInstance; + +public class DependencyDeclaration { + + public final String key; + + // unmodifiableList + public final List appConfigs; + + public final CreatePolicy createPolicy; + public final UpdatePolicy updatePolicy; + public final DeletePolicy deletePolicy; + + public final DependencyUpdatePolicy dependencyUpdatePolicy; + public final DependencyDeletePolicy dependencyDeletePolicy; + + public DependencyDeclaration(String key, CreatePolicy createPolicy, UpdatePolicy updatePolicy, + DeletePolicy deletePolicy, DependencyUpdatePolicy dependencyUpdatePolicy, + DependencyDeletePolicy dependencyDeletePolicy, AppDependencyConfig... appConfigs) { + this.key = key; + + if (appConfigs.length == 0) { + throw new IllegalArgumentException("There has to be atleast one 'appConfig'!"); + } + // TODO check for duplicated appIds + this.appConfigs = Collections.unmodifiableList(Arrays.asList(appConfigs)); + + this.createPolicy = createPolicy; + this.updatePolicy = updatePolicy; + this.deletePolicy = deletePolicy; + this.dependencyUpdatePolicy = dependencyUpdatePolicy; + this.dependencyDeletePolicy = dependencyDeletePolicy; + } + + public static class AppDependencyConfig { + + // NOTE: must have either appId or specificInstanceId + public final String appId; + public final UUID specificInstanceId; + public final String alias; + public final JsonObject properties; + + private AppDependencyConfig(String appId, UUID specificInstanceId, String alias, JsonObject properties) { + if (appId == null && specificInstanceId == null) { + throw new NullPointerException( + "'appId' and 'specificInstanceId' of a AppDependencyConfig can't be both null!"); + } + this.appId = appId; + this.specificInstanceId = specificInstanceId; + this.alias = alias; + this.properties = properties == null ? new JsonObject() : properties; + } + + /** + * Gets a {@link Builder} for an {@link AppDependencyConfig}. + * + * @return the builder + */ + public static Builder create() { + return new Builder(); + } + + public static final class Builder { + private String appId; + private UUID specificInstanceId; + private String alias; + private JsonObject properties; + + public Builder() { + } + + public Builder setAppId(String appId) { + this.appId = appId; + return this; + } + + public Builder setSpecificInstanceId(UUID specificInstanceId) { + this.specificInstanceId = specificInstanceId; + return this; + } + + public Builder setAlias(String alias) { + this.alias = alias; + return this; + } + + public Builder setProperties(JsonObject properties) { + this.properties = properties; + return this; + } + + public AppDependencyConfig build() { + return new AppDependencyConfig(this.appId, this.specificInstanceId, this.alias, this.properties); + } + } + + } + + /** + * Defines if the dependency app should get created when creating the parent + * app. + */ + public static enum CreatePolicy { + /** + * Always creates the dependent app except an {@link OpenemsAppInstance} is + * already created and not a dependency of another app. + */ + ALWAYS((instances, app) -> true), // + + /** + * lazy singleton. + */ + IF_NOT_EXISTING((instances, app) -> instances.stream().anyMatch(t -> t.appId.equals(app))), // + + /** + * Never allowed to create the app. + */ + NEVER((instances, app) -> false), // + ; + + private final BiFunction, String, Boolean> isAllowedToCreateFunction; + + private CreatePolicy(BiFunction, String, Boolean> isAllowedToCreateFunction) { + this.isAllowedToCreateFunction = isAllowedToCreateFunction; + } + + /** + * Determines if the app of the given appId is allowed to create. This does not + * mean an existing app can't be used as the dependency. + * + * @param allInstances all app instances + * @param appId the appId + * @return s true if the app is allowed to create + */ + public final boolean isAllowedToCreate(List allInstances, String appId) { + return this.isAllowedToCreateFunction.apply(allInstances, appId); + } + } + + /** + * Defines if the dependency should get updated when updating the parent app. + */ + public static enum UpdatePolicy { + ALWAYS(v -> true), // + IF_MINE(v -> !v.allInstances.stream() // + .filter(i -> !i.equals(v.parent)) // + .anyMatch(a -> a.dependencies != null + && a.dependencies.stream().anyMatch(d -> d.instanceId.equals(v.app2Update.instanceId)))), // + NEVER(v -> false), // + ; + + private final Function isAllowedToUpdateFunction; + + private UpdatePolicy(Function isAllowedToUpdate) { + this.isAllowedToUpdateFunction = isAllowedToUpdate; + } + + /** + * Determines if an {@link OpenemsAppInstance} is allowed to be updated. + * + * @param allInstances all {@link OpenemsAppInstance} + * @param parent the parent {@link OpenemsAppInstance} + * @param app2Update the {@link OpenemsAppInstance} to updated + * @return true if the instance is allowed to be updated else false + */ + public final boolean isAllowedToUpdate(List allInstances, OpenemsAppInstance parent, + OpenemsAppInstance app2Update) { + return this.isAllowedToUpdateFunction.apply(new AllowedToValues(allInstances, parent, app2Update)); + } + + } + + /** + * Defines if the dependency app gets deleted when deleting its parent. + */ + public static enum DeletePolicy { + ALWAYS(v -> true), // + IF_MINE(v -> !v.allInstances.stream().filter(a -> !a.equals(v.parent) && a.dependencies != null) + .anyMatch(a -> a.dependencies.stream().anyMatch(d -> d.instanceId.equals(v.app2Update.instanceId)))), // + NEVER(v -> false), // + ; + + private final Function isAllowedToDeleteFunction; + + private DeletePolicy(Function isAllowedToDelete) { + this.isAllowedToDeleteFunction = isAllowedToDelete; + } + + /** + * Determines if an {@link OpenemsAppInstance} is allowed to be deleted. + * + * @param allInstances all {@link OpenemsAppInstance} + * @param parent the parent {@link OpenemsAppInstance} + * @param app2Delete the {@link OpenemsAppInstance} to delete + * @return true if the instance is allowed to be deleted else false + */ + public final boolean isAllowedToDelete(List allInstances, OpenemsAppInstance parent, + OpenemsAppInstance app2Delete) { + return this.isAllowedToDeleteFunction.apply(new AllowedToValues(allInstances, parent, app2Delete)); + } + } + + private static class AllowedToValues { + public final List allInstances; + public final OpenemsAppInstance parent; + public final OpenemsAppInstance app2Update; + + public AllowedToValues(List allInstances, OpenemsAppInstance parent, + OpenemsAppInstance app2Update) { + this.allInstances = allInstances; + this.parent = parent; + this.app2Update = app2Update; + } + } + + /** + * Defines if the user can change properties of the dependency app. + */ + public static enum DependencyUpdatePolicy { + ALLOW_ALL, // + ALLOW_ONLY_UNCONFIGURED_PROPERTIES, // + ALLOW_NONE, // + ; + } + + /** + * Defines if the user can delete an app which is a dependency of another app. + */ + public static enum DependencyDeletePolicy { + NOT_ALLOWED, // + ALLOWED, // + ; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/DependencyUtil.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/DependencyUtil.java new file mode 100644 index 00000000000..d1a8fb51186 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/DependencyUtil.java @@ -0,0 +1,62 @@ +package io.openems.edge.core.appmanager.dependency; + +import java.util.UUID; + +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppManager; +import io.openems.edge.core.appmanager.AppManagerImpl; + +public class DependencyUtil { + + /** + * Temporary field to avoid endless loop. + */ + private static boolean isCurrentlyRunning = false; + + /** + * Gets the instanceId of the first found app that has the given componentId in + * its {@link AppConfiguration}. + * + *

+ * NOTE: when calling this inside an app configuration it can lead to an endless + * loop + * + * @param componentManager a componentManager to get the appManager + * @param componentId the component id that the app should have + * @param currentlyCallingApp the app that is currently calling this methode + * @return the found instanceId or null if no app has this component + */ + public static final UUID getInstanceIdOfAppWhichHasComponent(ComponentManager componentManager, String componentId, + String currentlyCallingApp) { + if (isCurrentlyRunning) { + return null; + } + isCurrentlyRunning = true; + var appManagerImpl = DependencyUtil.getAppManagerImpl(componentManager); + if (appManagerImpl == null) { + isCurrentlyRunning = false; + return null; + } + for (var entry : appManagerImpl.appConfigs()) { + if (entry.getValue().components.stream().anyMatch(c -> c.getId().equals(componentId))) { + isCurrentlyRunning = false; + return entry.getKey().instanceId; + } + } + isCurrentlyRunning = false; + return null; + } + + private static final AppManagerImpl getAppManagerImpl(ComponentManager componentManager) { + var appManager = componentManager.getEnabledComponentsOfType(AppManager.class); + if (appManager.size() != 1) { + return null; + } + if (!(appManager.get(0) instanceof AppManagerImpl)) { + return null; + } + return (AppManagerImpl) appManager.get(0); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/ExistingDependencyConfig.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/ExistingDependencyConfig.java new file mode 100644 index 00000000000..20964573368 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/ExistingDependencyConfig.java @@ -0,0 +1,22 @@ +package io.openems.edge.core.appmanager.dependency; + +import java.util.List; + +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppInstance; + +public class ExistingDependencyConfig extends DependencyConfig { + + // @Nullable + public final OpenemsAppInstance parentInstance; + public final OpenemsAppInstance instance; + + public ExistingDependencyConfig(OpenemsApp app, OpenemsApp parentApp, DependencyDeclaration sub, + AppConfiguration config, DependencyDeclaration.AppDependencyConfig appDependencyConfig, + List declarations, OpenemsAppInstance parent, OpenemsAppInstance instance) { + super(app, parentApp, sub, config, appDependencyConfig, declarations); + this.parentInstance = parent; + this.instance = instance; + } +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/SchedulerAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/SchedulerAggregateTaskImpl.java new file mode 100644 index 00000000000..d3642058500 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/SchedulerAggregateTaskImpl.java @@ -0,0 +1,68 @@ +package io.openems.edge.core.appmanager.dependency; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.user.User; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.ComponentUtil; + +@Component +public class SchedulerAggregateTaskImpl implements AggregateTask, AggregateTask.SchedulerAggregateTask { + + private final AggregateTask.ComponentAggregateTask aggregateTask; + private final ComponentUtil componentUtil; + + private List order; + private List removeIds; + + @Activate + public SchedulerAggregateTaskImpl(@Reference AggregateTask.ComponentAggregateTask aggregateTask, + @Reference ComponentUtil componentUtil) { + this.aggregateTask = aggregateTask; + this.componentUtil = componentUtil; + } + + @Override + public void reset() { + this.order = new LinkedList<>(); + this.removeIds = new LinkedList<>(); + } + + @Override + public void aggregate(AppConfiguration instance, AppConfiguration oldConfig) { + if (instance != null) { + this.order = this.componentUtil.insertSchedulerOrder(this.order, instance.schedulerExecutionOrder); + } + if (oldConfig != null) { + var schedulerIdDiff = new ArrayList<>(oldConfig.schedulerExecutionOrder); + if (instance != null) { + schedulerIdDiff.removeAll(instance.schedulerExecutionOrder); + } + this.removeIds.addAll(schedulerIdDiff); + } + } + + @Override + public void create(User user, List otherAppConfigurations) throws OpenemsNamedException { + this.order = this.componentUtil.insertSchedulerOrder(this.componentUtil.getSchedulerIds(), this.order); + this.componentUtil.updateScheduler(user, this.order, this.aggregateTask.getCreatedComponents()); + + this.delete(user, otherAppConfigurations); + } + + @Override + public void delete(User user, List otherAppConfigurations) throws OpenemsNamedException { + this.removeIds.addAll(this.aggregateTask.getDeletedComponents()); + this.removeIds.removeAll(AppManagerAppHelperImpl.getSchedulerIdsFromConfigs(otherAppConfigurations)); + + this.componentUtil.removeIdsInSchedulerIfExisting(user, this.removeIds); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/StaticIpAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/StaticIpAggregateTaskImpl.java new file mode 100644 index 00000000000..fc048b7dd29 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/StaticIpAggregateTaskImpl.java @@ -0,0 +1,73 @@ +package io.openems.edge.core.appmanager.dependency; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.user.User; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.ComponentUtil; + +@Component +public class StaticIpAggregateTaskImpl implements AggregateTask, AggregateTask.StaticIpAggregateTask { + + private final boolean isWindows = System.getProperty("os.name").startsWith("Windows"); + + private final ComponentUtil componentUtil; + + private List ips; + private List ips2Delete; + + @Activate + public StaticIpAggregateTaskImpl(@Reference ComponentUtil componentUtil) { + this.componentUtil = componentUtil; + + } + + @Override + public void reset() { + this.ips = new LinkedList<>(); + this.ips2Delete = new LinkedList<>(); + } + + @Override + public void aggregate(AppConfiguration instance, AppConfiguration oldConfig) { + if (this.isWindows) { + return; + } + if (instance != null) { + this.ips.addAll(instance.ips); + } + if (oldConfig != null) { + var diff = new ArrayList<>(oldConfig.ips); + if (instance != null) { + diff.removeAll(instance.ips); + } + this.ips2Delete.addAll(diff); + } + } + + @Override + public void create(User user, List otherAppConfigurations) throws OpenemsNamedException { + if (this.isWindows) { + return; + } + + this.componentUtil.updateHosts(user, this.ips, this.ips2Delete); + } + + @Override + public void delete(User user, List otherAppConfigurations) throws OpenemsNamedException { + if (System.getProperty("os.name").startsWith("Windows")) { + return; + } + this.ips.removeAll(AppManagerAppHelperImpl.getStaticIpsFromConfigs(otherAppConfigurations)); + this.componentUtil.updateHosts(user, null, this.ips2Delete); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/UpdateValues.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/UpdateValues.java new file mode 100644 index 00000000000..57ae5f41f12 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/UpdateValues.java @@ -0,0 +1,28 @@ +package io.openems.edge.core.appmanager.dependency; + +import java.util.List; + +import io.openems.edge.core.appmanager.OpenemsAppInstance; + +public class UpdateValues { + + public final OpenemsAppInstance rootInstance; + public final List modifiedOrCreatedApps; + public final List deletedApps; + + public final List warnings; + + public UpdateValues(OpenemsAppInstance rootInstance, List modifiedOrCreatedApps, + List deletedApps) { + this(rootInstance, modifiedOrCreatedApps, deletedApps, null); + } + + public UpdateValues(OpenemsAppInstance rootInstance, List modifiedOrCreatedApps, + List deletedApps, List warnings) { + this.rootInstance = rootInstance; + this.modifiedOrCreatedApps = modifiedOrCreatedApps; + this.deletedApps = deletedApps; + this.warnings = warnings; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/translation_de.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/translation_de.properties new file mode 100644 index 00000000000..dd1c1846fc5 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/translation_de.properties @@ -0,0 +1,9 @@ +appNotAllowedToBeUpdated = Diese App darf nicht geupdated werden wegen einer Abhängigkeit einer anderen App! +appNotAllowedToBeDeleted = Diese App darf nicht gelöscht werden wegen einer Abhängigkeit einer anderen App! +canNotChangeProperty = Feld[{0}] darf nicht geändert werden wegen einer Abhängigkeit einer anderen App! +canNotChangeAlias = Alias darf nicht geändert werden wegen einer Abhängigkeit einer anderen App! +overrideProperty = Feld[{0}] wird überschrieben wegen einer Abhängigkeit einer anderen App! +canNotUpdateComponents = Komponenten konnten nicht geupdated werden! +canNotUpdateScheduler = Ausführungsreihenfolge im Scheduler konnte nicht geupdated werden! +canNotUpdateStaticIps = Statische IPs konnte nicht geupdated werden! +canNotGetAppConfiguration = AppConfiguration konnte nicht geholt werden! \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/translation_en.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/translation_en.properties new file mode 100644 index 00000000000..b900ca96b80 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/translation_en.properties @@ -0,0 +1,9 @@ +appNotAllowedToBeUpdated = This app is not allowed to be updated because of a dependency constraint! +appNotAllowedToBeDeleted = App is not allowed to be deleted because of a dependency constraint! +canNotChangeProperty = Can not change Property[{0}] because of a Dependency constraint! +canNotChangeAlias = Can not change Alias because of a Dependency constraint! +overrideProperty = Override Property[{0}] because of a Dependency constraint! +canNotUpdateComponents = Can not update Components! +canNotUpdateScheduler = Can not update Scheduler! +canNotUpdateStaticIps = Can not update static ips! +canNotGetAppConfiguration = Can not get AppConfiguration! \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/AddAppInstance.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/AddAppInstance.java index cd582efd6fc..e84b46efc74 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/AddAppInstance.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/AddAppInstance.java @@ -1,8 +1,11 @@ package io.openems.edge.core.appmanager.jsonrpc; +import java.util.List; import java.util.UUID; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.jsonrpc.base.JsonrpcRequest; @@ -36,7 +39,8 @@ * "jsonrpc": "2.0", * "id": "UUID", * "result": { - * "instanceId": string (uuid) + * "instance": {@link OpenemsAppInstance#toJsonObject()} + * "warnings": string[] * } * } * @@ -92,17 +96,21 @@ public JsonObject getParams() { public static class Response extends JsonrpcResponseSuccess { - private final UUID instanceId; + private final OpenemsAppInstance instance; + private final JsonArray warnings; - public Response(UUID id, UUID instanceId) { + public Response(UUID id, OpenemsAppInstance instance, List warnings) { super(id); - this.instanceId = instanceId; + this.instance = instance; + this.warnings = warnings == null ? new JsonArray() + : warnings.stream().map(JsonPrimitive::new).collect(JsonUtils.toJsonArray()); } @Override public JsonObject getResult() { return JsonUtils.buildJsonObject() // - .addProperty("instanceId", this.instanceId.toString()) // + .add("instance", this.instance.toJsonObject()) // + .add("warnings", this.warnings) // .build(); } } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/DeleteAppInstance.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/DeleteAppInstance.java index 1c26278d423..3806cd6339a 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/DeleteAppInstance.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/DeleteAppInstance.java @@ -1,11 +1,15 @@ package io.openems.edge.core.appmanager.jsonrpc; +import java.util.List; import java.util.UUID; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.common.utils.JsonUtils; import io.openems.edge.core.appmanager.OpenemsAppInstance; @@ -33,7 +37,9 @@ * { * "jsonrpc": "2.0", * "id": "UUID", - * "result": {} + * "result": { + * "warnings": string[] + * } * } * */ @@ -76,4 +82,22 @@ public JsonObject getParams() { } } + public static class Response extends JsonrpcResponseSuccess { + + private final JsonArray warnings; + + public Response(UUID id, List warnings) { + super(id); + this.warnings = warnings == null ? new JsonArray() + : warnings.stream().map(JsonPrimitive::new).collect(JsonUtils.toJsonArray()); + } + + @Override + public JsonObject getResult() { + return JsonUtils.buildJsonObject() // + .add("warnings", this.warnings) // + .build(); + } + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetApp.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetApp.java index 5caa797bb11..0f40a78ed63 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetApp.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetApp.java @@ -8,9 +8,11 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.jsonrpc.base.JsonrpcRequest; import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; +import io.openems.common.session.Language; import io.openems.common.utils.JsonUtils; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppInstance; +import io.openems.edge.core.appmanager.validator.Validator; /** * Gets the available {@link OpenemsApp}. @@ -95,7 +97,8 @@ public JsonObject getParams() { public static class Response extends JsonrpcResponseSuccess { - private static JsonObject createAppObject(OpenemsApp app, List instantiatedApps) { + private static JsonObject createAppObject(OpenemsApp app, List instantiatedApps, + Language language, Validator validator) { var instanceIds = JsonUtils.buildJsonArray(); for (var instantiatedApp : instantiatedApps) { @@ -103,24 +106,25 @@ private static JsonObject createAppObject(OpenemsApp app, List instantiatedApps) { + public Response(UUID id, OpenemsApp app, List instantiatedApps, Language language, + Validator validator) { super(id); - this.app = createAppObject(app, instantiatedApps); + this.app = createAppObject(app, instantiatedApps, language, validator); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetAppInstances.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetAppInstances.java index 1595c3489b4..86299849aa0 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetAppInstances.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetAppInstances.java @@ -89,11 +89,7 @@ public static class Response extends JsonrpcResponseSuccess { public Response(UUID id, List instances) { super(id); - var result = JsonUtils.buildJsonArray(); // - for (var instance : instances) { - result.add(instance.toJsonObject()); - } - this.instances = result.build(); + this.instances = instances.stream().map(OpenemsAppInstance::toJsonObject).collect(JsonUtils.toJsonArray()); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetApps.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetApps.java index c96efa33124..b18b418c3d2 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetApps.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/GetApps.java @@ -10,9 +10,11 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.jsonrpc.base.JsonrpcRequest; import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; +import io.openems.common.session.Language; import io.openems.common.utils.JsonUtils; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppInstance; +import io.openems.edge.core.appmanager.validator.Validator; /** * Gets the available {@link OpenemsApp}s. @@ -92,7 +94,7 @@ public JsonObject getParams() { public static class Response extends JsonrpcResponseSuccess { private static JsonArray createAppsArray(List availableApps, - List instantiatedApps) { + List instantiatedApps, Language language, Validator validator) { var result = JsonUtils.buildJsonArray(); for (var app : availableApps) { // TODO don't show integrated systems for normal users @@ -108,15 +110,15 @@ private static JsonArray createAppsArray(List availableApps, } var categorys = JsonUtils.buildJsonArray().build(); for (var cat : app.getCategorys()) { - categorys.add(cat.toJsonObject()); + categorys.add(cat.toJsonObject(language)); } result.add(JsonUtils.buildJsonObject() // .add("categorys", categorys) // .addProperty("cardinality", app.getCardinality().name()) // .addProperty("appId", app.getAppId()) // - .addProperty("name", app.getName()) // + .addProperty("name", app.getName(language)) // .addProperty("image", app.getImage()) // - .add("status", app.getValidator().toJsonObject()) // + .add("status", validator.toJsonObject(app.getValidatorConfig(), language)) // .add("instanceIds", instanceIds.build()) // .build()); } @@ -125,9 +127,10 @@ private static JsonArray createAppsArray(List availableApps, private final JsonArray apps; - public Response(UUID id, List availableApps, List instantiatedApps) { + public Response(UUID id, List availableApps, List instantiatedApps, + Language language, Validator validator) { super(id); - this.apps = createAppsArray(availableApps, instantiatedApps); + this.apps = createAppsArray(availableApps, instantiatedApps, language, validator); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/UpdateAppInstance.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/UpdateAppInstance.java index a63e7522783..8b0c531dca9 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/UpdateAppInstance.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/jsonrpc/UpdateAppInstance.java @@ -1,11 +1,15 @@ package io.openems.edge.core.appmanager.jsonrpc; +import java.util.List; import java.util.UUID; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.common.utils.JsonUtils; import io.openems.edge.core.appmanager.OpenemsAppInstance; @@ -34,7 +38,10 @@ * { * "jsonrpc": "2.0", * "id": "UUID", - * "result": {} + * "result": { + * "instance": {@link OpenemsAppInstance#toJsonObject()} + * "warnings": string[] + * } * } * */ @@ -70,9 +77,10 @@ private Request(JsonrpcRequest request, UUID instanceId, String alias, JsonObjec this.properties = properties; } - public Request(UUID instanceId, JsonObject properties) { + public Request(UUID instanceId, String alias, JsonObject properties) { super(METHOD); this.instanceId = instanceId; + this.alias = alias; this.properties = properties; } @@ -80,9 +88,31 @@ public Request(UUID instanceId, JsonObject properties) { public JsonObject getParams() { return JsonUtils.buildJsonObject() // .addProperty("instanceId", this.instanceId.toString()) // + .addProperty("alias", this.alias) // .add("properties", this.properties) // .build(); } } + public static class Response extends JsonrpcResponseSuccess { + + private final OpenemsAppInstance instance; + private final JsonArray warnings; + + public Response(UUID id, OpenemsAppInstance instance, List warnings) { + super(id); + this.instance = instance; + this.warnings = warnings == null ? new JsonArray() + : warnings.stream().map(JsonPrimitive::new).collect(JsonUtils.toJsonArray()); + } + + @Override + public JsonObject getResult() { + return JsonUtils.buildJsonObject() // + .add("instance", this.instance.toJsonObject()) // + .add("warnings", this.warnings) // + .build(); + } + } + } 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 new file mode 100644 index 00000000000..7db987b2709 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties @@ -0,0 +1,158 @@ +# Categories +integratedSystems = Integrierte Systeme +timeOfUseTariff = Zeitvariable Stromtarife +evcs = E-Mobilität +heat = Wärme +loadControl = Laststeuerung +hardware = Hardware +pvInverter = PV-Wechselrichter +pvSelfConsumption = PV-Eigenverbrauch +meter = Zähler +api = Schnittstellen + +# Global +ipAddress = IP-Adresse +port = Port +modbusUnitId = Modbus Unit-ID +modbusUnitId.description = Die Unit-ID von den Modbus Gerät. +germany = Deutschland +austria = Österreich +switzerland = Schweiz +username = Benutzername +password = Passwort + +# Api +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.ReadOnly.Name = Modbus/TCP lesend + +App.Api.ModbusTcp.ReadWrite.Name = 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.Mqtt.Name = MQTT-Api lesend +App.Api.Mqtt.Username.description = Benutzername für die Authentifizierung beim MQTT-Broker. +App.Api.Mqtt.Password.description = Passwort für die Authentifizierung beim MQTT-Broker. +App.Api.Mqtt.EdgeId.label = Client-ID +App.Api.Mqtt.EdgeId.description = Client-ID für die Authentifizierung beim MQTT-Broker. +App.Api.Mqtt.Uri.description = Die Verbindungs Uri zum MQTT-Broker. + +App.Api.RestJson.ReadOnly.Name = REST/JSON lesend +App.Api.RestJson.ReadWrite.Name = REST/JSON Schreibzugriff + +# Evcs +App.Evcs.Cluster.Name = Multiladepunkt-Management +App.Evcs.Cluster.evcsIds.description = IDs von Ladestationen. + +App.Evcs.HardyBarth.Name = eCharge Hardy Barth Ladestation +App.Evcs.HardyBarth.Ip.description = Die IP-Adresse der Ladestation. Wenn die Ladestation zwei Anschlüsse hat, hat der zweite/slave Anschluss die IP 192.168.25.31. + +App.Evcs.IesKeywatt.Name = IES Keywatt Ladestation +App.Evcs.IesKeywatt.chargepoint.label = OCPP Zapfsäulen-Kennung +App.Evcs.IesKeywatt.chargepoint.description = Die OCPP-Kennung der Ladestation. +App.Evcs.IesKeywatt.connector.label = OCPP Stecker-Kennung +App.Evcs.IesKeywatt.connector.description = Die Anschlusskennung der Stromzapfsäule (z. B. wenn es zwei Anschlüsse gibt, hat die Stromzapfsäule zwei Kennungen 1 und 2). + +App.Evcs.Keba.Name = KEBA Ladestation +App.Evcs.Keba.Ip.description = Die IP-Adresse der Ladestation. + +# Hardware +App.Hardware.KMtronic8Channel.Name = FEMS Relais 8-Kanal +App.Hardware.KMtronic8Channel.Ip.description = Die IP-Adresse des Relais. + +# Heat +App.Heat.CHP.Name = Blockheizkraftwerk (BHKW) +App.Heat.CHP.outputChannel.label = Ausgangskanal +App.Heat.CHP.outputChannel.description = Kanaladresse des digitalen Ausgangs. +App.Heat.HeatingElement.Name = Heizstab +App.Heat.HeatingElement.outputChannelPhaseL1.label = Ausgangskanal Phase L1 +App.Heat.HeatingElement.outputChannelPhaseL1.description = Kanaladresse des digitalen Ausgangs für Phase L1. +App.Heat.HeatingElement.outputChannelPhaseL2.label = Ausgangskanal Phase L2 +App.Heat.HeatingElement.outputChannelPhaseL2.description = Kanaladresse des digitalen Ausgangs für Phase L2. +App.Heat.HeatingElement.outputChannelPhaseL3.label = Ausgangskanal Phase L3 +App.Heat.HeatingElement.outputChannelPhaseL3.description = Kanaladresse des digitalen Ausgangs für Phase L3. +App.Heat.HeatPump.Name = "SG-Ready" Wärmepumpe +App.Heat.HeatPump.outputChannel1.label = Ausgangskanal 1 +App.Heat.HeatPump.outputChannel1.description = Kanaladresse des digitalen Ausgangs für Eingang 1. +App.Heat.HeatPump.outputChannel2.label = Ausgangskanal 2 +App.Heat.HeatPump.outputChannel2.description = Kanaladresse des digitalen Ausgangs für Eingang 2. + +# Load control +App.LoadControl.ThresholdControl.Name = Schwellwertsteuerung +App.LoadControl.ThresholdControl.outputChannels.label = Ausgangskanal Adressen +App.LoadControl.ThresholdControl.outputChannels.description = Kanaladresse der digitalen Ausgänge, die geschaltet werden sollen. +App.LoadControl.ManualRelayControl.Name = Manuelle Relaissteuerung +App.LoadControl.ManualRelayControl.outputChannel.label = Ausgangskanal Adressen +App.LoadControl.ManualRelayControl.outputChannel.description = Kanaladresse des digitalen Ausgangs, der geschaltet werden soll. + +# Integrated System +App.FENECON.Home.Name = FENECON Home +App.FENECON.Home.safetyCountry.label = Batterie-Wechselrichter Ländereinstellung +App.FENECON.Home.rippleControlReceiver.label = Rundsteuerempfänger aktiviert? +App.FENECON.Home.rippleControlReceiver.description = Externe Abregelung durch Netzbetreiber +App.FENECON.Home.feedInLimit.label = Begrenzung der Einspeisung [W] +App.FENECON.Home.feedInSettings.label = Einspeise-Einstellungen +App.FENECON.Home.hasAcMeterSocomec.label = Hat AC-Zähler (SOCOMEC) +App.FENECON.Home.hasDcPV1.label = Hat DC-PV 1 (MPPT 1) +App.FENECON.Home.hasDcPV2.label = Hat DC-PV 2 (MPPT 2) +App.FENECON.Home.emergencyPowerSupply.label = Aktivieren der Notstromversorgung +App.FENECON.Home.emergencyPowerEnergy.label = Aktivieren der Notfallreserve Energie +App.FENECON.Home.reserveEnergy.label = Notfallreserve Energie (State-of-Charge) + +App.FENECON.Home.modbus0.alias = Kommunikation mit der Batterie +App.FENECON.Home.modbus1.alias = Kommunikation mit dem Batterie-Wechselrichter +App.FENECON.Home.meter0.alias = Netzzähler +App.FENECON.Home.io0.alias = Relaisboard +App.FENECON.Home.battery0.alias = Batterie +App.FENECON.Home.batteryInverter0.alias = Batterie-Wechselrichter +App.FENECON.Home.ess0.alias = Speichersystem +App.FENECON.Home.predictor0.alias = Prognose +App.FENECON.Home.ctrlEssSurplusFeedToGrid0.alias = Überschusseinspeisung +App.FENECON.Home.ctrlBalancing0.alias = Eigenverbrauchsoptimierung +App.FENECON.Home.meter1.alias = AC Zähler +App.FENECON.Home.meter2.alias = Notstromverbraucher +App.FENECON.Home.ctrlEmergencyCapacityReserve0.alias = Ansteuerung der Notstromreserve + +# Meter +App.Meter.mountType.label = Einbindungs Typ +App.Meter.ip.description = Die IP-Adresse des Messgeräts. +App.Meter.production = Erzeugung +App.Meter.gridMeter = Netzzähler +App.Meter.consumtionMeter = Verbrauchszähler + +App.Meter.CarloGavazzi.Name = CARLO GAVAZZI Zähler +App.Meter.Janitza.Name = Janitza Zähler +App.Meter.Janitza.productModel = Produkt Model +App.Meter.Socomec.Name = SOCOMEC Zähler + +# PV-Inverter +App.PvInverter.ip.description = Die IP-Adresse des PV-Wechselrichters. +App.PvInverter.port.description = Der Port des PV-Wechselrichters. + +App.PvInverter.Kaco.Name = KACO PV-Wechselrichter +App.PvInverter.Kostal.Name = KOSTAL PV-Wechselrichter +App.PvInverter.Sma.Name = SMA PV-Wechselrichter +App.PvInverter.Sma.modbusUnitId.description = Die Unit-ID des Modbus-Geräts. Beachten Sie, dass Sie laut Handbuch den Wert, den Sie in der SMA Webschnittstelle konfiguriert haben, um '123' ergänzen müssen. +App.PvInverter.SolarEdge.Name = SolarEdge PV-Wechselrichter + +# Time of use Tarif +App.TimeOfUseTariff.Awattar.Name = Awattar HOURLY +App.TimeOfUseTariff.Stromdao.Name = Stromdao Corrently +App.TimeOfUseTariff.Stromdao.zipCode.label = Postleitzahl +App.TimeOfUseTariff.Stromdao.zipCode.description = Deutsche Postleitzahl des Ortes. +App.TimeOfUseTariff.Tibber.Name = Tibber +App.TimeOfUseTariff.Tibber.accessToken.label = Zugangstoken +App.TimeOfUseTariff.Tibber.accessToken.description = Zugangstoken für den Tibber Stromtarif. + +# PvSelfConsumption +App.PvSelfConsumption.GridOptimizedCharge.Name = Netzdienliche Beladung +App.PvSelfConsumption.GridOptimizedCharge.sellToGridLimitEnabled.label = Ist der maximal Stromverkauf an das Netz aktiviert? +App.PvSelfConsumption.GridOptimizedCharge.sellToGridLimitEnabled.description = Ist die Logik für den maximal Stromverkauf an das Netz aktiviert? +App.PvSelfConsumption.GridOptimizedCharge.maximumSellToGridPower.label = Maximal zulässiger Stromverkauf an das Netz +App.PvSelfConsumption.GridOptimizedCharge.maximumSellToGridPower.description = Die Zielgrenze für den Verkauf von Strom an das Netz. +App.PvSelfConsumption.SelfConsumptionOptimization.Name = Eigenverbrauchsoptimierung +App.PvSelfConsumption.SelfConsumptionOptimization.ess.label = Ess-ID +App.PvSelfConsumption.SelfConsumptionOptimization.ess.description = ID von den Ess Gerät. +App.PvSelfConsumption.SelfConsumptionOptimization.meter.label = Netzzähler-ID +App.PvSelfConsumption.SelfConsumptionOptimization.meter.description = ID von den Netzzähler. \ No newline at end of file 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 new file mode 100644 index 00000000000..b508bb6c13e --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties @@ -0,0 +1,158 @@ +# Categories +integratedSystems = Integrated Systems +timeOfUseTariff = Time +evcs = e-mobility +heat = Heat +loadControl = Load control +hardware = Hardware +pvInverter = PV-Inverter +pvSelfConsumption = PV self-consumption +meter = Meter +api = API's + +# Global +ipAddress = IP-Address +port = Port +modbusUnitId = Modbus Unit-ID +modbusUnitId.description = The Unit-ID of the Modbus device. +germany = Germany +austria = Austria +switzerland = Switzerland +username = Username +password = Password + +# Api +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.ModbusTcp.ReadOnly.Name = Modbus/TCP reading + +App.Api.ModbusTcp.ReadWrite.Name = 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.Mqtt.Name = MQTT-Api reading +App.Api.Mqtt.Username.description = Username for authentication at MQTT broker. +App.Api.Mqtt.Password.description = Password for authentication at MQTT broker. +App.Api.Mqtt.EdgeId.label = Client-ID +App.Api.Mqtt.EdgeId.description = Client-ID for authentication at MQTT broker. +App.Api.Mqtt.Uri.description = The connection Uri to MQTT broker. + +App.Api.RestJson.ReadOnly.Name = REST/JSON reading +App.Api.RestJson.ReadWrite.Name = REST/JSON Write Access + +# Evcs +App.Evcs.Cluster.Name = Multi-charge point management +App.Evcs.Cluster.evcsIds.description = IDs of EVCS devices. + +App.Evcs.HardyBarth.Name = eCharge Hardy Barth Charging Station +App.Evcs.HardyBarth.Ip.description = The IP address of the charging station. If the charger has two connectors, the second/slave evcs has the IP 192.168.25.31. + +App.Evcs.IesKeywatt.Name = IES Keywatt Charging Station +App.Evcs.IesKeywatt.chargepoint.label = OCPP chargepoint identifier +App.Evcs.IesKeywatt.chargepoint.description = The OCPP identifier of the charging station. +App.Evcs.IesKeywatt.connector.label = OCPP connector identifier +App.Evcs.IesKeywatt.connector.description = The connector id of the chargepoint (e.g. if there are two connectors, then the evcs has two id's 1 and 2). + +App.Evcs.Keba.Name = KEBA Charging Station +App.Evcs.Keba.Ip.description = The IP address of the charging station. + +# Hardware +App.Hardware.KMtronic8Channel.Name = FEMS Relay 8-Channel +App.Hardware.KMtronic8Channel.Ip.description = The IP address of the Relay. + +# Heat +App.Heat.CHP.Name = Combined heat and power plant (CHP) +App.Heat.CHP.outputChannel.label = Output Channel Address +App.Heat.CHP.outputChannel.description = Channel address of the Digital Output that should be switched. +App.Heat.HeatingElement.Name = Heating rod +App.Heat.HeatingElement.outputChannelPhaseL1.label = Output Channel Phase L1 +App.Heat.HeatingElement.outputChannelPhaseL1.description = Channel address of the Digital Output for Phase L1. +App.Heat.HeatingElement.outputChannelPhaseL2.label = Output Channel Phase L2 +App.Heat.HeatingElement.outputChannelPhaseL2.description = Channel address of the Digital Output for Phase L2. +App.Heat.HeatingElement.outputChannelPhaseL3.label = Output Channel Phase L3 +App.Heat.HeatingElement.outputChannelPhaseL3.description = Channel address of the Digital Output for Phase L3. +App.Heat.HeatPump.Name = "SG-Ready" Heat Pump +App.Heat.HeatPump.outputChannel1.label = Output Channel 1 +App.Heat.HeatPump.outputChannel1.description = Channel address of the Digital Output for input 1. +App.Heat.HeatPump.outputChannel2.label = Output Channel 2 +App.Heat.HeatPump.outputChannel2.description = Channel address of the Digital Output for input 2. + +# Load control +App.LoadControl.ThresholdControl.Name = Threshold Control +App.LoadControl.ThresholdControl.outputChannels.label = Output Channels +App.LoadControl.ThresholdControl.outputChannels.description = Channel addresses of the Digital Outputs that should be switched. +App.LoadControl.ManualRelayControl.Name = Manual Relay Control +App.LoadControl.ManualRelayControl.outputChannel.label = Output Channel +App.LoadControl.ManualRelayControl.outputChannel.description = Channel address of the Digital Output that should be switched. + +# Integrated System +App.FENECON.Home.Name = FENECON Home +App.FENECON.Home.safetyCountry.label = Battery-Inverter Safety Country +App.FENECON.Home.rippleControlReceiver.label = Ripple control receiver active? +App.FENECON.Home.rippleControlReceiver.description = External balancing by grid operator +App.FENECON.Home.feedInLimit.label = Feed-In limitation [W] +App.FENECON.Home.feedInSettings.label = Feed-In Settings +App.FENECON.Home.hasAcMeterSocomec.label = Has AC meter (SOCOMEC) +App.FENECON.Home.hasDcPV1.label = Has DC-PV 1 (MPPT 1) +App.FENECON.Home.hasDcPV2.label = Has DC-PV 2 (MPPT 2) +App.FENECON.Home.emergencyPowerSupply.label = Activate Emergency power supply +App.FENECON.Home.emergencyPowerEnergy.label = Activate Emergency Reserve Energy +App.FENECON.Home.reserveEnergy.label = Emergency Reserve Energy (State-of-Charge) + +App.FENECON.Home.modbus0.alias = Communication with the battery +App.FENECON.Home.modbus1.alias = Communication with the battery inverter +App.FENECON.Home.meter0.alias = Grid meter +App.FENECON.Home.io0.alias = Relay +App.FENECON.Home.battery0.alias = battery +App.FENECON.Home.batteryInverter0.alias = battery inverter +App.FENECON.Home.ess0.alias = Storage system +App.FENECON.Home.predictor0.alias = Forecast +App.FENECON.Home.ctrlEssSurplusFeedToGrid0.alias = Excess feed-in +App.FENECON.Home.ctrlBalancing0.alias = Self-consumption optimization +App.FENECON.Home.meter1.alias = AC Meter +App.FENECON.Home.meter2.alias = Emergency power consumers +App.FENECON.Home.ctrlEmergencyCapacityReserve0.alias = Control of the emergency power reserve + +# Meter +App.Meter.mountType.label = Mount Type +App.Meter.ip.description = The IP address of the Meter. +App.Meter.production = Production +App.Meter.gridMeter = Grid-Meter +App.Meter.consumtionMeter = Consumption-Meter + +App.Meter.CarloGavazzi.Name = CARLO GAVAZZI Meter +App.Meter.Janitza.Name = Janitza Meter +App.Meter.Janitza.productModel = Product Model +App.Meter.Socomec.Name = SOCOMEC Meter + +# PV-Inverter +App.PvInverter.ip.description = The IP address of the PV-Inverter. +App.PvInverter.port.description = The port of the PV-Inverter. + +App.PvInverter.Kaco.Name = KACO PV-Inverter +App.PvInverter.Kostal.Name = KOSTAL PV-Inverter +App.PvInverter.Sma.Name = SMA PV Inverter +App.PvInverter.Sma.modbusUnitId.description = The Unit-ID of the Modbus device. Be aware, that according to the manual you need to add '123' to the value that you configured in the SMA web interface. +App.PvInverter.SolarEdge.Name = SolarEdge PV-Inverter + +# Time of use Tarif +App.TimeOfUseTariff.Awattar.Name = Awattar HOURLY +App.TimeOfUseTariff.Stromdao.Name = Stromdao Corrently +App.TimeOfUseTariff.Stromdao.zipCode.label = ZIP Code +App.TimeOfUseTariff.Stromdao.zipCode.description = German ZIP Code of the location. +App.TimeOfUseTariff.Tibber.Name = Tibber +App.TimeOfUseTariff.Tibber.accessToken.label = Access token +App.TimeOfUseTariff.Tibber.accessToken.description = Access token for the Tibber API. + +# PvSelfConsumption +App.PvSelfConsumption.GridOptimizedCharge.Name = Grid optimized charge +App.PvSelfConsumption.GridOptimizedCharge.sellToGridLimitEnabled.label = Is Sell-To-Grid-Limit enabled? +App.PvSelfConsumption.GridOptimizedCharge.sellToGridLimitEnabled.description = Is the sell to grid limit logic enabled? +App.PvSelfConsumption.GridOptimizedCharge.maximumSellToGridPower.label = Maximum allowed Sell-To-Grid power +App.PvSelfConsumption.GridOptimizedCharge.maximumSellToGridPower.description = The target limit for sell-to-grid power. +App.PvSelfConsumption.SelfConsumptionOptimization.Name = self-consumption optimisation +App.PvSelfConsumption.SelfConsumptionOptimization.ess.label = Ess-ID +App.PvSelfConsumption.SelfConsumptionOptimization.ess.description = ID of Ess device. +App.PvSelfConsumption.SelfConsumptionOptimization.meter.label = Grid-Meter-ID +App.PvSelfConsumption.SelfConsumptionOptimization.meter.description = ID of the Grid-Meter. \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/AbstractCheckable.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/AbstractCheckable.java new file mode 100644 index 00000000000..88b50059db5 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/AbstractCheckable.java @@ -0,0 +1,46 @@ +package io.openems.edge.core.appmanager.validator; + +import java.util.ResourceBundle; + +import org.osgi.service.component.ComponentConstants; +import org.osgi.service.component.ComponentContext; + +import io.openems.common.session.Language; +import io.openems.edge.core.appmanager.TranslationUtil; + +public abstract class AbstractCheckable implements Checkable { + + private final ComponentContext componentContext; + + public AbstractCheckable(ComponentContext componentContext) { + this.componentContext = componentContext; + } + + @Override + public String getComponentName() { + return this.componentContext.getProperties().get(ComponentConstants.COMPONENT_NAME).toString(); + } + + protected static String getTranslation(Language language, String key, Object... params) { + if (language == null) { + language = Language.DEFAULT; + } + // TODO translation + switch (language) { + case CZ: + case ES: + case FR: + case NL: + language = Language.EN; + break; + case DE: + case EN: + break; + } + + var translationBundle = ResourceBundle.getBundle("io.openems.edge.core.appmanager.validator.translation", + language.getLocal()); + return TranslationUtil.getTranslation(translationBundle, key, params); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckAppsNotInstalled.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckAppsNotInstalled.java index bace5c7528a..89914e4b73f 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckAppsNotInstalled.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckAppsNotInstalled.java @@ -5,15 +5,17 @@ import java.util.Map; import java.util.stream.Collectors; +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 io.openems.common.session.Language; import io.openems.edge.core.appmanager.AppManager; import io.openems.edge.core.appmanager.AppManagerImpl; @Component(name = CheckAppsNotInstalled.COMPONENT_NAME) -public class CheckAppsNotInstalled implements Checkable { +public class CheckAppsNotInstalled extends AbstractCheckable implements Checkable { public static final String COMPONENT_NAME = "Validator.Checkable.CheckAppsNotInstalled"; @@ -23,7 +25,8 @@ public class CheckAppsNotInstalled implements Checkable { private List installedApps = new LinkedList<>(); @Activate - public CheckAppsNotInstalled(@Reference AppManager appManager) { + public CheckAppsNotInstalled(@Reference AppManager appManager, ComponentContext componentContext) { + super(componentContext); this.appManager = appManager; } @@ -35,14 +38,10 @@ public void setProperties(Map properties) { @Override public boolean check() { this.installedApps = new LinkedList<>(); - if (this.appManager == null) { - return false; - } - if (!(this.appManager instanceof AppManagerImpl)) { + var appManagerImpl = this.getAppManagerImpl(); + if (appManagerImpl == null) { return false; } - var appManagerImpl = (AppManagerImpl) this.appManager; - var instances = appManagerImpl.getInstantiatedApps(); for (String item : this.appIds) { if (instances.stream().anyMatch(t -> t.appId.equals(item))) { @@ -52,10 +51,20 @@ public boolean check() { return this.installedApps.isEmpty(); } + private AppManagerImpl getAppManagerImpl() { + if (this.appManager == null) { + return null; + } + if (!(this.appManager instanceof AppManagerImpl)) { + return null; + } + return (AppManagerImpl) this.appManager; + } + @Override - public String getErrorMessage() { - return "Apps with ID[" + this.installedApps.stream().collect(Collectors.joining(", ")) + "] are installed!" - + System.lineSeparator() + "Delete them to be able to install this App."; + public String getErrorMessage(Language language) { + return AbstractCheckable.getTranslation(language, "Validator.Checkable.CheckAppsNotInstalled.Message", + this.installedApps.stream().collect(Collectors.joining(", "))); } } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckCardinality.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckCardinality.java index b7ce09ad056..ad0d6d9069d 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckCardinality.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckCardinality.java @@ -2,11 +2,14 @@ import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; +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 io.openems.common.session.Language; import io.openems.edge.core.appmanager.AppManager; import io.openems.edge.core.appmanager.AppManagerImpl; import io.openems.edge.core.appmanager.OpenemsApp; @@ -15,17 +18,28 @@ import io.openems.edge.core.appmanager.OpenemsAppInstance; @Component(name = CheckCardinality.COMPONENT_NAME) -public class CheckCardinality implements Checkable { +public class CheckCardinality extends AbstractCheckable implements Checkable { public static final String COMPONENT_NAME = "Validator.Checkable.CheckCardinality"; private final AppManager appManager; private OpenemsApp openemsApp; - private String errorMessage = null; + private ErrorType errorType = ErrorType.NONE; + private String errorMessage; + private OpenemsAppCategory matchingCategory; + + private static enum ErrorType { + SAME_CATEGORIE, // + SAME_APP, // + NONE, // + OTHER, // + ; + } @Activate - public CheckCardinality(@Reference AppManager appManager) { + public CheckCardinality(@Reference AppManager appManager, ComponentContext componentContext) { + super(componentContext); this.appManager = appManager; } @@ -36,13 +50,16 @@ public void setProperties(Map properties) { @Override public boolean check() { + this.errorType = ErrorType.NONE; this.errorMessage = null; if (this.appManager == null) { this.errorMessage = "App Manager not available!"; + this.errorType = ErrorType.OTHER; return false; } if (!(this.appManager instanceof AppManagerImpl)) { this.errorMessage = "Wrong AppManager active!"; + this.errorType = ErrorType.OTHER; return false; } var appManagerImpl = (AppManagerImpl) this.appManager; @@ -52,48 +69,64 @@ public boolean check() { case SINGLE: if (instantiatedApps.stream().anyMatch(t -> t.appId.equals(this.openemsApp.getAppId()))) { // only create one instance of this app - this.errorMessage = "An instance of the app[" + this.openemsApp.getAppId() + "] is already created!"; + this.errorType = ErrorType.SAME_APP; } break; case SINGLE_IN_CATEGORY: var matchedCategorie = this.getMatchingCategorie(appManagerImpl, instantiatedApps); if (matchedCategorie != null) { // only create one instance with the same category of this app - this.errorMessage = "An instance of an app with the same category[" + matchedCategorie.name() - + "] is already created!"; + this.matchingCategory = matchedCategorie; + this.errorType = ErrorType.SAME_CATEGORIE; } break; case MULTIPLE: // any number of this app can be instantiated break; - default: - this.errorMessage = "Usage '" + this.openemsApp.getCardinality().name() + "' is not implemented."; } - return this.errorMessage == null; + return this.errorType == ErrorType.NONE; } private OpenemsAppCategory getMatchingCategorie(AppManagerImpl appManager, List instantiatedApps) { for (var openemsAppInstance : instantiatedApps) { - var app = appManager.findAppById(openemsAppInstance.appId); - if (app.getCardinality() != OpenemsAppCardinality.SINGLE_IN_CATEGORY) { - continue; - } - for (var cat : app.getCategorys()) { - for (var catOther : this.openemsApp.getCategorys()) { - if (cat == catOther) { - return cat; + try { + var app = appManager.findAppById(openemsAppInstance.appId); + if (app.getCardinality() != OpenemsAppCardinality.SINGLE_IN_CATEGORY) { + continue; + } + for (var cat : app.getCategorys()) { + for (var catOther : this.openemsApp.getCategorys()) { + if (cat == catOther) { + return cat; + } } } + } catch (NoSuchElementException e) { + // if app instance is reworked there may be no app for the instance + continue; } } return null; } @Override - public String getErrorMessage() { - return this.errorMessage; + public String getErrorMessage(Language language) { + switch (this.errorType) { + case SAME_APP: + return AbstractCheckable.getTranslation(language, "Validator.Checkable.CheckCardinality.Message.Single", + this.openemsApp.getAppId()); + case SAME_CATEGORIE: + return AbstractCheckable.getTranslation(language, + "Validator.Checkable.CheckCardinality.Message.SingleInCategorie", + this.matchingCategory.getReadableName(language)); + case OTHER: + return this.errorMessage; + case NONE: + return null; + } + return null; } } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHome.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHome.java index 31bf80d82c0..e8b53cc5280 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHome.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHome.java @@ -2,15 +2,17 @@ import java.util.TreeMap; +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 io.openems.common.OpenemsConstants; +import io.openems.common.session.Language; import io.openems.edge.common.component.ComponentManager; @Component(name = CheckHome.COMPONENT_NAME) -public class CheckHome implements Checkable { +public class CheckHome extends AbstractCheckable implements Checkable { public static final String COMPONENT_NAME = "Validator.Checkable.CheckHome"; @@ -18,9 +20,10 @@ public class CheckHome implements Checkable { private final Checkable checkAppsNotInstalled; @Activate - public CheckHome(@Reference ComponentManager componentManager, + public CheckHome(@Reference ComponentManager componentManager, ComponentContext componentContext, @Reference(target = "(" + OpenemsConstants.PROPERTY_OSGI_COMPONENT_NAME + "=" + CheckAppsNotInstalled.COMPONENT_NAME + ")") Checkable checkAppsNotInstalled) { + super(componentContext); this.componentManager = componentManager; this.checkAppsNotInstalled = checkAppsNotInstalled; } @@ -28,21 +31,21 @@ public CheckHome(@Reference ComponentManager componentManager, @Override public boolean check() { var batteries = this.componentManager.getEdgeConfig().getComponentsByFactory("Battery.Fenecon.Home"); - this.checkAppsNotInstalled.setProperties(new Validator.MapBuilder<>(new TreeMap()) // + this.checkAppsNotInstalled.setProperties(new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("appIds", new String[] { "App.FENECON.Home" }) // .build()); // TODO remove check for batteries // not every home has the home app installed but if a batterie of an home is // installed its probably a home and so the app can be used. // later there should only be checked if the home app is installed because if - // the configuration is wrong there may no home battery installed and so the app - // wouldn't be available even though it is a home + // the configuration is wrong there may be no home battery installed and so the + // app wouldn't be available even though it is a home return !batteries.isEmpty() || !this.checkAppsNotInstalled.check(); } @Override - public String getErrorMessage() { - return "No Home installed"; + public String getErrorMessage(Language language) { + return AbstractCheckable.getTranslation(language, "Validator.Checkable.CheckHome.Message"); } } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHost.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHost.java index 45cbacc4325..8d88c319866 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHost.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHost.java @@ -6,11 +6,14 @@ import java.net.UnknownHostException; import java.util.Map; +import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import io.openems.common.session.Language; + @Component(name = CheckHost.COMPONENT_NAME) -public class CheckHost implements Checkable { +public class CheckHost extends AbstractCheckable implements Checkable { public static final String COMPONENT_NAME = "Validator.Checkable.CheckHost"; @@ -18,7 +21,8 @@ public class CheckHost implements Checkable { private Integer port; @Activate - public CheckHost() { + public CheckHost(ComponentContext componentContext) { + super(componentContext); } private void init(String host, Integer port) { @@ -64,13 +68,15 @@ public boolean check() { } @Override - public String getErrorMessage() { - // TODO translation - var portMsg = this.port != null ? " on Port " + this.port : ""; + public String getErrorMessage(Language language) { + var address = this.host.getHostAddress(); + if (this.port != null) { + address += ":" + this.port; + } if (this.host == null) { - return "IP '" + this.host.getHostAddress() + "'" + portMsg + " is not a valid IP-Address"; + return AbstractCheckable.getTranslation(language, "Validator.Checkable.CheckHost.WrongIp", address); } - return "Device with IP '" + this.host.getHostAddress() + "'" + portMsg + " is not reachable!"; + return AbstractCheckable.getTranslation(language, "Validator.Checkable.CheckHost.NotReachable", address); } } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckNoComponentInstalledOfFactoryId.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckNoComponentInstalledOfFactoryId.java index 9b771ad9011..42739394733 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckNoComponentInstalledOfFactoryId.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckNoComponentInstalledOfFactoryId.java @@ -2,14 +2,16 @@ import java.util.Map; +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 io.openems.common.session.Language; import io.openems.edge.common.component.ComponentManager; @Component(name = CheckNoComponentInstalledOfFactoryId.COMPONENT_NAME) -public class CheckNoComponentInstalledOfFactoryId implements Checkable { +public class CheckNoComponentInstalledOfFactoryId extends AbstractCheckable implements Checkable { public static final String COMPONENT_NAME = "Validator.Checkable.CheckNoComponentInstalledOfFactorieId"; @@ -18,7 +20,9 @@ public class CheckNoComponentInstalledOfFactoryId implements Checkable { private String factorieId; @Activate - public CheckNoComponentInstalledOfFactoryId(@Reference ComponentManager componentManager) { + public CheckNoComponentInstalledOfFactoryId(@Reference ComponentManager componentManager, + ComponentContext componentContext) { + super(componentContext); this.componentManager = componentManager; } @@ -33,9 +37,9 @@ public boolean check() { } @Override - public String getErrorMessage() { - return "Components with the FactorieID[" + this.factorieId + "] are installed!" + System.lineSeparator() - + "Remove them to be able to install this app."; + public String getErrorMessage(Language language) { + return AbstractCheckable.getTranslation(language, + "Validator.Checkable.CheckNoComponentInstalledOfFactorieId.Message", this.factorieId); } } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckRelayCount.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckRelayCount.java index 665b60a2d7c..c5566b7ad39 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckRelayCount.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckRelayCount.java @@ -4,15 +4,17 @@ import java.util.List; import java.util.Map; +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 io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.session.Language; import io.openems.edge.core.appmanager.ComponentUtil; @Component(name = CheckRelayCount.COMPONENT_NAME) -public class CheckRelayCount implements Checkable { +public class CheckRelayCount extends AbstractCheckable implements Checkable { public static final String COMPONENT_NAME = "Validator.Checkable.CheckRelayCount"; @@ -21,7 +23,8 @@ public class CheckRelayCount implements Checkable { private int count; @Activate - public CheckRelayCount(@Reference ComponentUtil openemsAppUtil) { + public CheckRelayCount(@Reference ComponentUtil openemsAppUtil, ComponentContext componentContext) { + super(componentContext); this.openemsAppUtil = openemsAppUtil; } @@ -58,9 +61,8 @@ public boolean check() { } @Override - public String getErrorMessage() { - // TODO translation - return "There are not enough Relay ports available!"; + public String getErrorMessage(Language language) { + return AbstractCheckable.getTranslation(language, "Validator.Checkable.CheckRelayCount.Message"); } } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkable.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkable.java index 312424e424a..883dde7d8c7 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkable.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkable.java @@ -2,8 +2,17 @@ import java.util.Map; +import io.openems.common.session.Language; + public interface Checkable { + /** + * Gets the Component Name of the {@link Checkable}. + * + * @return the component name + */ + public String getComponentName(); + /** * Checks if the implemented task was successful or not. * @@ -14,9 +23,10 @@ public interface Checkable { /** * Gets the error message if the check was incorrect completed. * + * @param language the language of the message * @return the message */ - public String getErrorMessage(); + public String getErrorMessage(Language language); /** * Sets the properties. diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Validator.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Validator.java index 427ca4b66f0..3f541095ff0 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Validator.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Validator.java @@ -1,260 +1,83 @@ package io.openems.edge.core.appmanager.validator; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.framework.ServiceReference; - -import com.google.common.collect.Lists; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; -import io.openems.common.OpenemsConstants; -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.exceptions.OpenemsException; -import io.openems.common.function.ThrowingBiFunction; +import io.openems.common.session.Language; import io.openems.common.utils.JsonUtils; -import io.openems.edge.core.appmanager.ConfigurationTarget; - -public class Validator { - - private static final Logger LOG = Logger.getLogger(Validator.class.getName()); - - private final Map> compatibleCheckableNames; - private final Map> installableCheckableNames; - - private ThrowingBiFunction>, // - OpenemsNamedException> // - configurationValidation; - - public static final class Builder { - - private Map> compatibleCheckableNames; - private Map> installableCheckableNames; - - protected Builder() { +import io.openems.edge.core.appmanager.validator.ValidatorConfig.CheckableConfig; - } - - public Builder setCompatibleCheckableNames(Map> compatibleCheckableNames) { - this.compatibleCheckableNames = compatibleCheckableNames; - return this; - } - - public Builder setInstallableCheckableNames(Map> installableCheckableNames) { - this.installableCheckableNames = installableCheckableNames; - return this; - } - - public Validator build() { - return new Validator(this.compatibleCheckableNames, this.installableCheckableNames); - } - - } - - public static final class MapBuilder, K, V> { - - private final T map; - - public MapBuilder(T mapImpl) { - this.map = mapImpl; - } - - /** - * Does the exact same like {@link Map#put(Object, Object)}. - * - * @param key the key - * @param value the value - * @return this - */ - public MapBuilder put(K key, V value) { - this.map.put(key, value); - return this; - } - - public T build() { - return this.map; - } - } - - /** - * Creates a builder for an {@link Validator}. - * - * @return the builder - */ - public static final Builder create() { - return new Builder(); - } - - protected Validator(Map> compatibleCheckableNames, - Map> installableCheckableNames) { - this.compatibleCheckableNames = compatibleCheckableNames != null // - ? compatibleCheckableNames - : new HashMap<>(); - this.installableCheckableNames = installableCheckableNames != null // - ? installableCheckableNames - : new HashMap<>(); - - } +public interface Validator { /** * Gets the error messages for compatibility. * + * @param config the config that gets validated + * @param language the language of the errors * @return the error messages */ - public List getErrorCompatibleMessages() { - return getErrorMessages(this.compatibleCheckableNames, false); - } - - /** - * Gets the error messages for the given {@link Checkable}. - * - * @param checkableNames the {@link Checkable} to be checked. - * @param returnImmediate after the first checkable who returns false - * @return a list of errors - */ - private static List getErrorMessages(Map> checkableNames, boolean returnImmediate) { - if (checkableNames == null || checkableNames.isEmpty()) { - return new ArrayList<>(); - } - var errorMessages = new ArrayList(checkableNames.size()); - var bundleContext = FrameworkUtil.getBundle(Checkable.class).getBundleContext(); - // build filter - var filterBuilder = new StringBuilder(); - if (checkableNames.size() > 1) { - filterBuilder.append("(|"); - } - checkableNames.entrySet().forEach(t -> filterBuilder.append("(component.name=" + t.getKey() + ")")); - if (checkableNames.size() > 1) { - filterBuilder.append(")"); - } - try { - // get all service references - Collection> serviceReferences = bundleContext - .getServiceReferences(Checkable.class, filterBuilder.toString()); - var noneExistingCheckables = Lists.newArrayList(); - checkableNames.forEach((t, u) -> noneExistingCheckables.add(t)); - var isReturnedImmediate = false; - for (var reference : serviceReferences) { - var componentName = (String) reference.getProperty(OpenemsConstants.PROPERTY_OSGI_COMPONENT_NAME); - var properties = checkableNames.get(componentName); - var checkable = bundleContext.getService(reference); - if (properties != null) { - checkable.setProperties(properties); - } - noneExistingCheckables.remove(componentName); - if (!checkable.check()) { - errorMessages.add(checkable.getErrorMessage()); - if (returnImmediate) { - isReturnedImmediate = true; - break; - } - } - } - - if (!noneExistingCheckables.isEmpty() && !isReturnedImmediate) { - LOG.log(Level.WARNING, "Checkables[" + noneExistingCheckables.stream().collect(Collectors.joining(";")) - + "] are not found!"); - } - - // free all service references - for (var reference : serviceReferences) { - bundleContext.ungetService(reference); - } - } catch (InvalidSyntaxException | IllegalStateException e) { - // Can not get service references - e.printStackTrace(); - } - return errorMessages; + public default List getErrorCompatibleMessages(ValidatorConfig config, Language language) { + return this.getErrorMessages(config.getCompatibleCheckableConfigs(), language, false); } /** * Gets the error messages for installation. * + * @param config the config that gets validated + * @param language the language of the errors * @return the error messages */ - public List getErrorInstallableMessages() { - return getErrorMessages(this.installableCheckableNames, false); + public default List getErrorInstallableMessages(ValidatorConfig config, Language language) { + return this.getErrorMessages(config.getInstallableCheckableConfigs(), language, false); } /** * Validates the {@link Checkable}s and gets the Status. * + * @param config the config that gets validated * @return the Status */ - public OpenemsAppStatus getStatus() { - if (!getErrorMessages(this.compatibleCheckableNames, true).isEmpty()) { + public default OpenemsAppStatus getStatus(ValidatorConfig config) { + // language not need for status + if (!this.getErrorMessages(config.getCompatibleCheckableConfigs(), Language.DEFAULT, true).isEmpty()) { return OpenemsAppStatus.INCOMPATIBLE; } - if (!getErrorMessages(this.installableCheckableNames, true).isEmpty()) { + if (!this.getErrorMessages(config.getInstallableCheckableConfigs(), Language.DEFAULT, true).isEmpty()) { return OpenemsAppStatus.COMPATIBLE; } return OpenemsAppStatus.INSTALLABLE; } - public void setConfigurationValidation(ThrowingBiFunction>, OpenemsNamedException> configurationValidation) { - this.configurationValidation = configurationValidation; - } - /** - * Builds a {@link JsonObject} out of this {@link Validator}. + * Builds a {@link JsonObject} out of the given {@link ValidatorConfig}. * + * @param config the config that gets validated + * @param language the language of the errors * @return the {@link JsonObject} */ - public JsonObject toJsonObject() { - var compatibleMessages = JsonUtils.buildJsonArray().build(); - for (var message : this.getErrorCompatibleMessages()) { - compatibleMessages.add(message); - } - var installableMessages = JsonUtils.buildJsonArray().build(); - for (var message : this.getErrorInstallableMessages()) { - installableMessages.add(message); - } + public default JsonObject toJsonObject(ValidatorConfig config, Language language) { return JsonUtils.buildJsonObject() // - .addProperty("name", this.getStatus().name()) // - .add("errorCompatibleMessages", compatibleMessages) // - .add("errorInstallableMessages", installableMessages) // + .addProperty("name", this.getStatus(config).name()) // + .add("errorCompatibleMessages", + this.getErrorCompatibleMessages(config, language).stream().map(JsonPrimitive::new) + .collect(JsonUtils.toJsonArray())) // + .add("errorInstallableMessages", + this.getErrorInstallableMessages(config, language).stream().map(JsonPrimitive::new) + .collect(JsonUtils.toJsonArray())) // .build(); } /** - * Validates the Configuration {@link Checkable}s. + * Gets the error messages for the given {@link Checkable}. * - * @param target the target of the configuration - * @param properties the configuration properties - * @throws OpenemsNamedException on validation error + * @param checkableConfigs the {@link Checkable}s to be checked. + * @param language the language of the errors + * @param returnImmediate after the first checkable who returns false + * @return a list of errors */ - public void validateConfiguration(ConfigurationTarget target, JsonObject properties) throws OpenemsNamedException { - if (this.configurationValidation == null) { - return; - } - var checkables = this.configurationValidation.apply(target, properties); - if (checkables == null) { - return; - } - var errors = getErrorMessages(this.compatibleCheckableNames, false); - if (!errors.isEmpty()) { - throw new OpenemsException(errors.stream().collect(Collectors.joining(";"))); - } - } - - public Map> getCompatibleCheckableNames() { - return this.compatibleCheckableNames; - } - - public Map> getInstallableCheckableNames() { - return this.installableCheckableNames; - } + public List getErrorMessages(List checkableConfigs, Language language, + boolean returnImmediate); } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorConfig.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorConfig.java new file mode 100644 index 00000000000..159fbaec4a6 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorConfig.java @@ -0,0 +1,125 @@ +package io.openems.edge.core.appmanager.validator; + +import java.util.List; +import java.util.Map; + +import com.google.common.collect.Lists; + +public class ValidatorConfig { + + private final List compatibleCheckableConfigs; + private final List installableCheckableConfigs; + + public static final class Builder { + + private List compatibleCheckableConfigs; + private List installableCheckableConfigs; + + protected Builder() { + + } + + public Builder setCompatibleCheckableConfigs(List compatibleCheckableConfigs) { + this.compatibleCheckableConfigs = compatibleCheckableConfigs; + return this; + } + + public Builder setInstallableCheckableConfigs(List installableCheckableConfigs) { + this.installableCheckableConfigs = installableCheckableConfigs; + return this; + } + + public ValidatorConfig build() { + return new ValidatorConfig(this.compatibleCheckableConfigs, this.installableCheckableConfigs); + } + + } + + // TODO convert to record in java 17. + public static final class CheckableConfig { + + public final String checkableComponentName; + public final boolean invertResult; + public final Map properties; + + public CheckableConfig(String checkableComponentName, boolean invertResult, Map properties) { + this.checkableComponentName = checkableComponentName; + this.invertResult = invertResult; + this.properties = properties; + } + + public CheckableConfig(String checkableComponentName, Map properties) { + this(checkableComponentName, false, properties); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (this.getClass() != obj.getClass()) { + return false; + } + var other = (CheckableConfig) obj; + return java.util.Objects.equals(this.checkableComponentName, other.checkableComponentName) + && this.invertResult == other.invertResult; + } + + } + + public static final class MapBuilder, K, V> { + + private final T map; + + public MapBuilder(T mapImpl) { + this.map = mapImpl; + } + + /** + * Does the exact same like {@link Map#put(Object, Object)}. + * + * @param key the key + * @param value the value + * @return this + */ + public MapBuilder put(K key, V value) { + this.map.put(key, value); + return this; + } + + public T build() { + return this.map; + } + } + + /** + * Creates a builder for an {@link ValidatorConfig}. + * + * @return the builder + */ + public static final Builder create() { + return new Builder(); + } + + protected ValidatorConfig(List compatibleCheckableConfigs, + List installableCheckableConfigs) { + this.compatibleCheckableConfigs = compatibleCheckableConfigs != null // + ? compatibleCheckableConfigs + : Lists.newArrayList(); + this.installableCheckableConfigs = installableCheckableConfigs != null // + ? installableCheckableConfigs + : Lists.newArrayList(); + } + + public List getCompatibleCheckableConfigs() { + return this.compatibleCheckableConfigs; + } + + public List getInstallableCheckableConfigs() { + return this.installableCheckableConfigs; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorImpl.java new file mode 100644 index 00000000000..89ed92d038d --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorImpl.java @@ -0,0 +1,96 @@ +package io.openems.edge.core.appmanager.validator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.Lists; + +import io.openems.common.OpenemsConstants; +import io.openems.common.session.Language; +import io.openems.edge.core.appmanager.validator.ValidatorConfig.CheckableConfig; + +@Component +public class ValidatorImpl implements Validator { + + private static final Logger LOG = LoggerFactory.getLogger(ValidatorImpl.class); + + @Activate + public ValidatorImpl() { + } + + @Override + public List getErrorMessages(List checkableConfigs, Language language, + boolean returnImmediate) { + if (checkableConfigs.isEmpty()) { + return new ArrayList<>(); + } + var errorMessages = new ArrayList(checkableConfigs.size()); + var bundleContext = FrameworkUtil.getBundle(Checkable.class).getBundleContext(); + // build filter + var filterBuilder = new StringBuilder(); + if (checkableConfigs.size() > 1) { + filterBuilder.append("(|"); + } + checkableConfigs.forEach(t -> filterBuilder.append("(component.name=" + t.checkableComponentName + ")")); + if (checkableConfigs.size() > 1) { + filterBuilder.append(")"); + } + try { + // get all service references + Collection> serviceReferences = bundleContext + .getServiceReferences(Checkable.class, filterBuilder.toString()); + var noneExistingCheckables = Lists.newArrayList(); + checkableConfigs.forEach(c -> noneExistingCheckables.add(c)); + var isReturnedImmediate = false; + var usedReferencens = new ArrayList>(serviceReferences.size()); + for (var reference : serviceReferences) { + var componentName = (String) reference.getProperty(OpenemsConstants.PROPERTY_OSGI_COMPONENT_NAME); + var checkableConfig = checkableConfigs.stream() + .filter(c -> c.checkableComponentName.equals(componentName)).findFirst().orElse(null); + var checkable = bundleContext.getService(reference); + usedReferencens.add(reference); + if (checkableConfig.properties != null) { + checkable.setProperties(checkableConfig.properties); + } + noneExistingCheckables.removeIf(c -> c.equals(checkableConfig)); + var result = checkable.check(); + if (result == checkableConfig.invertResult) { + var errorMessage = checkable.getErrorMessage(language); + if (checkableConfig.invertResult) { + errorMessage = "Invert[" + errorMessage + "]"; + } + errorMessages.add(errorMessage); + if (returnImmediate) { + isReturnedImmediate = true; + break; + } + } + } + + if (!noneExistingCheckables.isEmpty() && !isReturnedImmediate) { + LOG.warn("Checkables[" + noneExistingCheckables.stream().map(c -> c.checkableComponentName) + .collect(Collectors.joining(";")) + "] are not found!"); + } + + // free all service references + for (var reference : usedReferencens) { + bundleContext.ungetService(reference); + } + } catch (InvalidSyntaxException | IllegalStateException e) { + // Can not get service references + e.printStackTrace(); + } + return errorMessages; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_de.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_de.properties new file mode 100644 index 00000000000..9b38445dec1 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_de.properties @@ -0,0 +1,13 @@ +Validator.Checkable.CheckAppsNotInstalled.Message = Apps mit der ID[{0}] sind installiert! Deinstalliere diese um diese App installieren zu können. + +Validator.Checkable.CheckCardinality.Message.SingleInCategorie = Eine Instanz einer App mit der selben Kategorie "{0}" ist bereits erstellt worden! +Validator.Checkable.CheckCardinality.Message.Single = Eine Instanz der App[{0}] ist bereits erstellt worden! + +Validator.Checkable.CheckHome.Message = Kein Home System installiert! + +Validator.Checkable.CheckHost.NotReachable = Gerät mit der IP "{0}" ist nicht erreichbar! +Validator.Checkable.CheckHost.WrongIp = IP "{0}" ist keine valide IP-Adresse! + +Validator.Checkable.CheckNoComponentInstalledOfFactorieId.Message = Komponenten mit der FaktorieID[{0}] sind installiert! Entferne diese um diese App installieren zu können. + +Validator.Checkable.CheckRelayCount.Message = Es sind nicht genug Relais ports verfügbar! Installiere ein Relais um diese App installieren zu können. diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_en.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_en.properties new file mode 100644 index 00000000000..23147b16cc1 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_en.properties @@ -0,0 +1,13 @@ +Validator.Checkable.CheckAppsNotInstalled.Message = Apps with ID[{0}] are installed! Delete them to be able to install this App. + +Validator.Checkable.CheckCardinality.Message.SingleInCategorie = An instance of an app with the same category '{0}' is already created! +Validator.Checkable.CheckCardinality.Message.Single = An instance of the app[{0}] is already created! + +Validator.Checkable.CheckHome.Message = No Home system installed! + +Validator.Checkable.CheckHost.NotReachable = Device with IP "{0}" is not reachable! +Validator.Checkable.CheckHost.WrongIp = IP "{0}" is not a valid IP-Address! + +Validator.Checkable.CheckNoComponentInstalledOfFactorieId.Message = Components with the FactorieID[{0}] are installed! Remove them to be able to install this app. + +Validator.Checkable.CheckRelayCount.Message = There are not enough Relay ports available! Install a Relay to be able to install this App. diff --git a/io.openems.edge.core/test/io/openems/edge/app/TestADependencyToC.java b/io.openems.edge.core/test/io/openems/edge/app/TestADependencyToC.java new file mode 100644 index 00000000000..d1cfffdc6d3 --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/app/TestADependencyToC.java @@ -0,0 +1,127 @@ +package io.openems.edge.app; + +import java.util.EnumMap; + +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.session.Language; +import io.openems.common.utils.EnumUtils; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.TestADependencyToC.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AppAssistant; +import io.openems.edge.core.appmanager.AppConfiguration; +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.dependency.DependencyDeclaration; +import io.openems.edge.core.appmanager.dependency.DependencyDeclaration.AppDependencyConfig; + +/** + * Test app for testing dependencies. + */ +@Component(name = "App.Test.TestADependencyToC") +public class TestADependencyToC extends AbstractOpenemsApp implements OpenemsApp { + + public static enum Property { + CREATE_POLICY, // + UPDATE_POLICY, // + DELETE_POLICY, // + DEPENDENCY_UPDATE_POLICY, // + DEPENDENCY_DELETE_POLICY, // + NUMBER + } + + @Activate + public TestADependencyToC(@Reference ComponentManager componentManager, ComponentContext context, + @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + super(componentManager, context, cm, componentUtil); + } + + @Override + public AppAssistant getAppAssistant(Language language) { + return AppAssistant.create(this.getName(language)) // + .build(); + } + + @Override + public AppDescriptor getAppDescriptor() { + return AppDescriptor.create() // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategorys() { + return new OpenemsAppCategory[] { OpenemsAppCategory.TEST }; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.MULTIPLE; + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, s) -> { + + var createPolicy = Enum.valueOf(DependencyDeclaration.CreatePolicy.class, + EnumUtils.getAsOptionalString(p, Property.CREATE_POLICY) + .orElse(DependencyDeclaration.CreatePolicy.IF_NOT_EXISTING.name())); + + var updatePolicy = Enum.valueOf(DependencyDeclaration.UpdatePolicy.class, + EnumUtils.getAsOptionalString(p, Property.UPDATE_POLICY) + .orElse(DependencyDeclaration.UpdatePolicy.ALWAYS.name())); + + var deletePolicy = Enum.valueOf(DependencyDeclaration.DeletePolicy.class, + EnumUtils.getAsOptionalString(p, Property.DELETE_POLICY) + .orElse(DependencyDeclaration.DeletePolicy.IF_MINE.name())); + + var dependencyUpdatePolicy = Enum.valueOf(DependencyDeclaration.DependencyUpdatePolicy.class, + EnumUtils.getAsOptionalString(p, Property.DEPENDENCY_UPDATE_POLICY) + .orElse(DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ALL.name())); + + var dependencyDeletePolicy = Enum.valueOf(DependencyDeclaration.DependencyDeletePolicy.class, + EnumUtils.getAsOptionalString(p, Property.DEPENDENCY_DELETE_POLICY) + .orElse(DependencyDeclaration.DependencyDeletePolicy.ALLOWED.name())); + + var number = EnumUtils.getAsOptionalInt(p, Property.NUMBER).orElse(0); + + var dependencies = Lists.newArrayList(new DependencyDeclaration("C", // + createPolicy, updatePolicy, deletePolicy, // + dependencyUpdatePolicy, dependencyDeletePolicy, // + AppDependencyConfig.create() // + .setAppId("App.Test.TestC") // + .setProperties(JsonUtils.buildJsonObject() // + .addProperty(TestC.Property.NUMBER.name(), number) // + .build()) + .build()) // + ); + + return new AppConfiguration(null, null, null, dependencies); + }; + } + + @Override + protected Class getPropertyClass() { + return Property.class; + } + + @Override + public String getName(Language language) { + return this.getAppId(); + } + +} diff --git a/io.openems.edge.core/test/io/openems/edge/app/TestBDependencyToC.java b/io.openems.edge.core/test/io/openems/edge/app/TestBDependencyToC.java new file mode 100644 index 00000000000..4233da8514d --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/app/TestBDependencyToC.java @@ -0,0 +1,103 @@ +package io.openems.edge.app; + +import java.util.EnumMap; + +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.session.Language; +import io.openems.common.utils.EnumUtils; +import io.openems.edge.app.TestBDependencyToC.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AppAssistant; +import io.openems.edge.core.appmanager.AppConfiguration; +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.dependency.DependencyDeclaration; +import io.openems.edge.core.appmanager.dependency.DependencyDeclaration.AppDependencyConfig; + +/** + * Test app for testing dependencies. + */ +@Component(name = "App.Test.TestBDependencyToC") +public class TestBDependencyToC extends AbstractOpenemsApp implements OpenemsApp { + + public static enum Property { + CREATE_POLICY + } + + @Activate + public TestBDependencyToC(@Reference ComponentManager componentManager, ComponentContext context, + @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + super(componentManager, context, cm, componentUtil); + } + + @Override + public AppAssistant getAppAssistant(Language language) { + return AppAssistant.create(this.getName(language)) // + .build(); + } + + @Override + public AppDescriptor getAppDescriptor() { + return AppDescriptor.create() // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategorys() { + return new OpenemsAppCategory[] { OpenemsAppCategory.TEST }; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.MULTIPLE; + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, p, s) -> { + + var createPolicy = Enum.valueOf(DependencyDeclaration.CreatePolicy.class, + EnumUtils.getAsOptionalString(p, Property.CREATE_POLICY) + .orElse(DependencyDeclaration.CreatePolicy.IF_NOT_EXISTING.name())); + + var dependencies = Lists.newArrayList(new DependencyDeclaration("c", // + createPolicy, // + DependencyDeclaration.UpdatePolicy.ALWAYS, // + DependencyDeclaration.DeletePolicy.IF_MINE, // + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ALL, // + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED, // + AppDependencyConfig.create() // + .setAppId("App.Test.TestC") // + .build()) // + ); + + return new AppConfiguration(null, null, null, dependencies); + }; + } + + @Override + protected Class getPropertyClass() { + return Property.class; + } + + @Override + public String getName(Language language) { + return this.getAppId(); + } + +} diff --git a/io.openems.edge.core/test/io/openems/edge/app/TestC.java b/io.openems.edge.core/test/io/openems/edge/app/TestC.java new file mode 100644 index 00000000000..edb968ee7bc --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/app/TestC.java @@ -0,0 +1,83 @@ +package io.openems.edge.app; + +import java.util.EnumMap; + +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.session.Language; +import io.openems.edge.app.TestC.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AppAssistant; +import io.openems.edge.core.appmanager.AppConfiguration; +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; + +/** + * Test app for testing dependencies. + */ +@Component(name = "App.Test.TestC") +public class TestC extends AbstractOpenemsApp implements OpenemsApp { + + public static enum Property { + NUMBER + } + + @Activate + public TestC(@Reference ComponentManager componentManager, ComponentContext componentContext, + @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + public AppAssistant getAppAssistant(Language language) { + return AppAssistant.create(this.getName(language)) // + .build(); + } + + @Override + public AppDescriptor getAppDescriptor() { + return AppDescriptor.create() // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategorys() { + return new OpenemsAppCategory[] { OpenemsAppCategory.TEST }; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.MULTIPLE; + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + return (t, u, s) -> { + return new AppConfiguration(); + }; + } + + @Override + protected Class getPropertyClass() { + return Property.class; + } + + @Override + public String getName(Language language) { + return this.getAppId(); + } + +} diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerAppHelperImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerAppHelperImplTest.java new file mode 100644 index 00000000000..221055dce6c --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerAppHelperImplTest.java @@ -0,0 +1,471 @@ +package io.openems.edge.core.appmanager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.concurrent.ExecutionException; + +import org.junit.Before; +import org.junit.Test; +import org.osgi.service.component.ComponentConstants; +import org.osgi.service.component.ComponentContext; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.TestADependencyToC; +import io.openems.edge.app.TestBDependencyToC; +import io.openems.edge.app.TestC; +import io.openems.edge.app.evcs.KebaEvcs; +import io.openems.edge.app.integratedsystem.FeneconHome; +import io.openems.edge.app.timeofusetariff.AwattarHourly; +import io.openems.edge.app.timeofusetariff.StromdaoCorrently; +import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.common.test.DummyComponentContext; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.common.test.DummyUser; +import io.openems.edge.common.user.User; +import io.openems.edge.core.appmanager.dependency.AppManagerAppHelperImpl; +import io.openems.edge.core.appmanager.dependency.ComponentAggregateTaskImpl; +import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; +import io.openems.edge.core.appmanager.dependency.SchedulerAggregateTaskImpl; +import io.openems.edge.core.appmanager.dependency.StaticIpAggregateTaskImpl; +import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; +import io.openems.edge.core.appmanager.jsonrpc.DeleteAppInstance; +import io.openems.edge.core.appmanager.jsonrpc.UpdateAppInstance; +import io.openems.edge.core.appmanager.validator.CheckCardinality; +import io.openems.edge.core.appmanager.validator.Validator; + +public class AppManagerAppHelperImplTest { + + private final User user = new DummyUser("1", "password", Language.DEFAULT, Role.ADMIN); + + private DummyConfigurationAdmin cm; + private DummyComponentManager componentManger; + private ComponentUtil componentUtil; + private Validator validator; + + private FeneconHome homeApp; + private KebaEvcs kebaEvcsApp; + private AwattarHourly awattarApp; + private StromdaoCorrently stromdao; + + private TestADependencyToC testAApp; + private TestBDependencyToC testBApp; + private TestC testCApp; + + private AppManagerImpl sut; + + @Before + public void beforeEach() throws Exception { + + this.cm = new DummyConfigurationAdmin(); + this.cm.getOrCreateEmptyConfiguration(AppManager.SINGLETON_SERVICE_PID); + + this.componentManger = new DummyComponentManager(); + this.componentManger.setConfigJson(JsonUtils.buildJsonObject() // + .add("components", JsonUtils.buildJsonObject() // + .add("scheduler0", JsonUtils.buildJsonObject() // + .addProperty("factoryId", "Scheduler.AllAlphabetically") // + .add("properties", JsonUtils.buildJsonObject() // + .addProperty("enabled", true) // + .add("controllers.ids", JsonUtils.buildJsonArray() // + .add("ctrlGridOptimizedCharge0") // + .add("ctrlEssSurplusFeedToGrid0") // + .add("ctrlBalancing0") // + .build()) // + .build()) // + .build()) // + .build()) // + .add("factories", JsonUtils.buildJsonObject() // + .build()) // + .build() // + ); + this.componentUtil = new ComponentUtilImpl(this.componentManger, this.cm); + + this.homeApp = new FeneconHome(this.componentManger, getComponentContext("App.FENECON.Home"), this.cm, + this.componentUtil); + this.kebaEvcsApp = new KebaEvcs(this.componentManger, getComponentContext("App.Evcs.Keba"), this.cm, + this.componentUtil); + this.awattarApp = new AwattarHourly(this.componentManger, getComponentContext("App.TimeVariablePrice.Awattar"), + this.cm, this.componentUtil); + this.stromdao = new StromdaoCorrently(this.componentManger, + getComponentContext("App.TimeVariablePrice.Stromdao"), this.cm, this.componentUtil); + + this.testAApp = new TestADependencyToC(this.componentManger, getComponentContext("App.Test.TestADependencyToC"), + this.cm, this.componentUtil); + + this.testBApp = new TestBDependencyToC(this.componentManger, getComponentContext("App.Test.TestBDependencyToC"), + this.cm, this.componentUtil); + + this.testCApp = new TestC(this.componentManger, getComponentContext("App.Test.TestC"), this.cm, + this.componentUtil); + + final var componentTask = new ComponentAggregateTaskImpl(this.componentManger); + final var schedulerTask = new SchedulerAggregateTaskImpl(componentTask, this.componentUtil); + final var staticIpTask = new StaticIpAggregateTaskImpl(this.componentUtil); + + this.sut = new AppManagerImpl(); + this.componentManger.addComponent(this.sut); + this.componentManger.setConfigurationAdmin(this.cm); + + var dummyValidator = new DummyValidator(); + dummyValidator.setCheckables(Lists + .newArrayList(new CheckCardinality(this.sut, getComponentContext(CheckCardinality.COMPONENT_NAME)))); + this.validator = dummyValidator; + + var appManagerAppHelper = new AppManagerAppHelperImpl(this.componentManger, this.componentUtil, this.validator, + componentTask, schedulerTask, staticIpTask); + + new ComponentTest(this.sut) // + .addReference("cm", this.cm) // + .addReference("componentManager", this.componentManger) // + .addReference("appHelper", appManagerAppHelper) // + .addReference("validator", this.validator) // + .addReference("availableApps", + ImmutableList.of(this.homeApp, this.kebaEvcsApp, this.awattarApp, this.stromdao, this.testAApp, + this.testBApp, this.testCApp)) // + .activate(MyConfig.create() // + .setApps(JsonUtils.buildJsonArray() // + .build().toString()) // + .build()); + + } + + @Test + public void testCreatePolicyIfNotExisting() throws OpenemsNamedException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("CREATE_POLICY", DependencyDeclaration.CreatePolicy.IF_NOT_EXISTING.name()) + .build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testBApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("CREATE_POLICY", DependencyDeclaration.CreatePolicy.IF_NOT_EXISTING.name()) + .build())); + + assertEquals(3, this.sut.getInstantiatedApps().size()); + } + + @Test + public void testCreatePolicyAlways() throws OpenemsNamedException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("CREATE_POLICY", DependencyDeclaration.CreatePolicy.ALWAYS.name()).build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testBApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("CREATE_POLICY", DependencyDeclaration.CreatePolicy.ALWAYS.name()).build())); + + assertEquals(4, this.sut.getInstantiatedApps().size()); + } + + @Test + public void testCreatePolicyNever() throws OpenemsNamedException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("CREATE_POLICY", DependencyDeclaration.CreatePolicy.NEVER.name()).build())); + + assertEquals(1, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testBApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("CREATE_POLICY", DependencyDeclaration.CreatePolicy.NEVER.name()).build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + } + + @Test + public void testUpdatePolicyNever() throws OpenemsNamedException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("UPDATE_POLICY", DependencyDeclaration.UpdatePolicy.NEVER.name()) // + .addProperty("NUMBER", 1) // + .build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + + this.sut.handleJsonrpcRequest(this.user, + new UpdateAppInstance.Request(this.getAppByAppId(this.testAApp.getAppId()).instanceId, "", + JsonUtils.buildJsonObject() // + .addProperty("UPDATE_POLICY", DependencyDeclaration.UpdatePolicy.NEVER.name()) // + .addProperty("NUMBER", 2) // + .build())); + + var instance = this.getAppByAppId(this.testCApp.getAppId()); + assertEquals(1, instance.properties.get(TestC.Property.NUMBER.name()).getAsInt()); + } + + @Test + public void testUpdatePolicyAlways() throws OpenemsNamedException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("UPDATE_POLICY", DependencyDeclaration.UpdatePolicy.ALWAYS.name()) // + .addProperty("NUMBER", 1) // + .build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testBApp.getAppId(), "", // + JsonUtils.buildJsonObject().build())); + + assertEquals(3, this.sut.getInstantiatedApps().size()); + + this.sut.handleJsonrpcRequest(this.user, + new UpdateAppInstance.Request(this.getAppByAppId(this.testAApp.getAppId()).instanceId, "", + JsonUtils.buildJsonObject() // + .addProperty("UPDATE_POLICY", DependencyDeclaration.UpdatePolicy.ALWAYS.name()) // + .addProperty("NUMBER", 2) // + .build())); + + var instance = this.getAppByAppId(this.testCApp.getAppId()); + assertEquals(2, instance.properties.get(TestC.Property.NUMBER.name()).getAsInt()); + } + + @Test + public void testUpdatePolicyIfMine() throws OpenemsNamedException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("UPDATE_POLICY", DependencyDeclaration.UpdatePolicy.IF_MINE.name()) // + .addProperty("NUMBER", 1) // + .build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + + this.sut.handleJsonrpcRequest(this.user, + new UpdateAppInstance.Request(this.getAppByAppId(this.testAApp.getAppId()).instanceId, "", + JsonUtils.buildJsonObject() // + .addProperty("UPDATE_POLICY", DependencyDeclaration.UpdatePolicy.IF_MINE.name()) // + .addProperty("NUMBER", 2) // + .build())); + + var instance = this.getAppByAppId(this.testCApp.getAppId()); + assertEquals(2, instance.properties.get(TestC.Property.NUMBER.name()).getAsInt()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testBApp.getAppId(), "", // + JsonUtils.buildJsonObject().build())); + + assertEquals(3, this.sut.getInstantiatedApps().size()); + + this.sut.handleJsonrpcRequest(this.user, + new UpdateAppInstance.Request(this.getAppByAppId(this.testAApp.getAppId()).instanceId, "", + JsonUtils.buildJsonObject() // + .addProperty("UPDATE_POLICY", DependencyDeclaration.UpdatePolicy.IF_MINE.name()) // + .addProperty("NUMBER", 3) // + .build())); + + instance = this.getAppByAppId(this.testCApp.getAppId()); + assertEquals(2, instance.properties.get(TestC.Property.NUMBER.name()).getAsInt()); + } + + @Test + public void testDeletePolicyNever() throws OpenemsNamedException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("DELETE_POLICY", DependencyDeclaration.DeletePolicy.NEVER.name()) // + .build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + + var instance = this.getAppByAppId(this.testAApp.getAppId()); + this.sut.handleDeleteAppInstanceRequest(this.user, new DeleteAppInstance.Request(instance.instanceId)); + + assertEquals(1, this.sut.getInstantiatedApps().size()); + } + + @Test + public void testDeletePolicyAlways() throws OpenemsNamedException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("DELETE_POLICY", DependencyDeclaration.DeletePolicy.ALWAYS.name()) // + .build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testBApp.getAppId(), "", // + JsonUtils.buildJsonObject().build())); + + assertEquals(3, this.sut.getInstantiatedApps().size()); + + var instance = this.getAppByAppId(this.testAApp.getAppId()); + this.sut.handleDeleteAppInstanceRequest(this.user, new DeleteAppInstance.Request(instance.instanceId)); + + assertEquals(1, this.sut.getInstantiatedApps().size()); + } + + @Test + public void testDeletePolicyIfMine() throws OpenemsNamedException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("DELETE_POLICY", DependencyDeclaration.DeletePolicy.IF_MINE.name()) // + .build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testBApp.getAppId(), "", // + JsonUtils.buildJsonObject().build())); + + assertEquals(3, this.sut.getInstantiatedApps().size()); + + var instance = this.getAppByAppId(this.testAApp.getAppId()); + this.sut.handleDeleteAppInstanceRequest(this.user, new DeleteAppInstance.Request(instance.instanceId)); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + } + + @Test + public void testDependencyDeletePolicyAllowed() throws OpenemsNamedException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("DEPENDENCY_DELETE_POLICY", + DependencyDeclaration.DependencyDeletePolicy.ALLOWED.name()) // + .build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + + var instance = this.getAppByAppId(this.testCApp.getAppId()); + this.sut.handleDeleteAppInstanceRequest(this.user, new DeleteAppInstance.Request(instance.instanceId)); + + assertEquals(1, this.sut.getInstantiatedApps().size()); + } + + @Test(expected = OpenemsNamedException.class) + public void testDependencyDeletePolicyNotAllowed() throws OpenemsNamedException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("DEPENDENCY_DELETE_POLICY", + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED.name()) // + .build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + + var instance = this.getAppByAppId(this.testCApp.getAppId()); + this.sut.handleDeleteAppInstanceRequest(this.user, new DeleteAppInstance.Request(instance.instanceId)); + } + + @Test + public void testDependencyUpdatePolicyAllowAll() + throws OpenemsNamedException, InterruptedException, ExecutionException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("DEPENDENCY_UPDATE_POLICY", + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ALL.name()) // + .addProperty("NUMBER", 1) // + .build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + + var newAlias = "newAppAlias"; + var completable = this.sut.handleJsonrpcRequest(this.user, + new UpdateAppInstance.Request(this.getAppByAppId(this.testCApp.getAppId()).instanceId, newAlias, + JsonUtils.buildJsonObject() // + .addProperty("NUMBER", 2) // + .build())); + + var result = completable.get().getResult(); + assertTrue(!result.has("warnings") || (result.get("warnings").getAsJsonArray().size() == 0)); + + var instance = this.getAppByAppId(this.testCApp.getAppId()); + assertEquals(newAlias, instance.alias); + assertEquals(2, instance.properties.get("NUMBER").getAsInt()); + } + + @Test(expected = OpenemsNamedException.class) + public void testDependencyUpdatePolicyAllowNone() + throws OpenemsNamedException, InterruptedException, ExecutionException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("DEPENDENCY_UPDATE_POLICY", + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_NONE.name()) // + .addProperty("NUMBER", 1) // + .build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + + var newAlias = "newAppAlias"; + this.sut.handleJsonrpcRequest(this.user, + new UpdateAppInstance.Request(this.getAppByAppId(this.testCApp.getAppId()).instanceId, newAlias, + JsonUtils.buildJsonObject() // + .addProperty("NUMBER", 2) // + .build())); + } + + @Test + public void testDependencyUpdatePolicyAllowOnlyUnconfiguredProperties() + throws OpenemsNamedException, InterruptedException, ExecutionException { + assertEquals(0, this.sut.getInstantiatedApps().size()); + + this.sut.handleAddAppInstanceRequest(this.user, new AddAppInstance.Request(this.testAApp.getAppId(), "", // + JsonUtils.buildJsonObject() // + .addProperty("DEPENDENCY_UPDATE_POLICY", + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ONLY_UNCONFIGURED_PROPERTIES.name()) // + .addProperty("NUMBER", 1) // + .build())); + + assertEquals(2, this.sut.getInstantiatedApps().size()); + + var newAlias = "newAppAlias"; + var completable = this.sut.handleJsonrpcRequest(this.user, + new UpdateAppInstance.Request(this.getAppByAppId(this.testCApp.getAppId()).instanceId, newAlias, + JsonUtils.buildJsonObject() // + .addProperty("NUMBER", 2) // + .build())); + + var result = completable.get().getResult(); + assertTrue(result.has("warnings")); + assertTrue(result.get("warnings").getAsJsonArray().size() > 0); + + var instance = this.getAppByAppId(this.testCApp.getAppId()); + assertEquals(newAlias, instance.alias); + assertEquals(1, instance.properties.get("NUMBER").getAsInt()); + + } + + private OpenemsAppInstance getAppByAppId(String appId) { + return this.sut.getInstantiatedApps().stream().filter(i -> i.appId.equals(appId)).findAny().get(); + } + + private static ComponentContext getComponentContext(String appId) { + Dictionary properties = new Hashtable<>(); + properties.put(ComponentConstants.COMPONENT_NAME, appId); + return new DummyComponentContext(properties); + } + +} diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplTest.java index b7510066f44..d15276c8ae6 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplTest.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplTest.java @@ -8,9 +8,9 @@ import java.util.Dictionary; import java.util.Hashtable; -import java.util.Map.Entry; import java.util.TreeMap; import java.util.UUID; +import java.util.Map.Entry; import org.junit.Before; import org.junit.Test; @@ -20,9 +20,12 @@ import com.google.common.collect.ImmutableList; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.session.Language; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.evcs.KebaEvcs; import io.openems.edge.app.integratedsystem.FeneconHome; +import io.openems.edge.app.pvselfconsumption.GridOptimizedCharge; +import io.openems.edge.app.pvselfconsumption.SelfConsumptionOptimization; import io.openems.edge.app.timeofusetariff.AwattarHourly; import io.openems.edge.app.timeofusetariff.StromdaoCorrently; import io.openems.edge.common.host.Host; @@ -30,8 +33,14 @@ import io.openems.edge.common.test.DummyComponentContext; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.core.appmanager.dependency.AggregateTask; +import io.openems.edge.core.appmanager.dependency.AppManagerAppHelperImpl; +import io.openems.edge.core.appmanager.dependency.ComponentAggregateTaskImpl; +import io.openems.edge.core.appmanager.dependency.SchedulerAggregateTaskImpl; +import io.openems.edge.core.appmanager.dependency.StaticIpAggregateTaskImpl; import io.openems.edge.core.appmanager.validator.CheckCardinality; import io.openems.edge.core.appmanager.validator.Validator; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; public class AppManagerImplTest { @@ -39,7 +48,12 @@ public class AppManagerImplTest { private DummyComponentManager componentManger; private ComponentUtil componentUtil; + private Validator validator = new DummyValidator(); + private FeneconHome homeApp; + private GridOptimizedCharge gridOptimizedCharge; + private SelfConsumptionOptimization selfConsumptionOptimization; + private KebaEvcs kebaEvcsApp; private AwattarHourly awattarApp; private StromdaoCorrently stromdao; @@ -49,13 +63,12 @@ public class AppManagerImplTest { @Before public void beforeEach() throws Exception { - this.cm = new DummyConfigurationAdmin(); - this.cm.getOrCreateEmptyConfiguration(AppManager.SINGLETON_SERVICE_PID); - final var essId = "ess0"; final var modbusIdInternal = "modbus0"; final var modbusIdExternal = "modbus1"; + final var meterId = "meter0"; + final var emergencyReserveEnabled = false; // Battery-Inverter Settings @@ -63,6 +76,9 @@ public void beforeEach() throws Exception { final var maxFeedInPower = 10000; final var feedInSetting = "LAGGING_0_95"; + this.cm = new DummyConfigurationAdmin(); + this.cm.getOrCreateEmptyConfiguration(AppManager.SINGLETON_SERVICE_PID); + this.componentManger = new DummyComponentManager(); this.componentManger.setConfigJson(JsonUtils.buildJsonObject() // .add("components", JsonUtils.buildJsonObject() // @@ -94,7 +110,7 @@ public void beforeEach() throws Exception { .addProperty("invalidateElementsAfterReadErrors", 1) // .build()) // .build()) // - .add("meter0", JsonUtils.buildJsonObject() // + .add(meterId, JsonUtils.buildJsonObject() // .addProperty("factoryId", "GoodWe.Grid-Meter") // .addProperty("alias", "Netzzähler") // .add("properties", JsonUtils.buildJsonObject() // @@ -230,6 +246,13 @@ public void beforeEach() throws Exception { this.homeApp = new FeneconHome(this.componentManger, getComponentContext("App.FENECON.Home"), this.cm, this.componentUtil); + + this.gridOptimizedCharge = new GridOptimizedCharge(this.componentManger, + getComponentContext("App.PvSelfConsumption.GridOptimizedCharge"), this.cm, this.componentUtil); + + this.selfConsumptionOptimization = new SelfConsumptionOptimization(this.componentManger, + getComponentContext("App.PvSelfConsumption.SelfConsumptionOptimization"), this.cm, this.componentUtil); + this.kebaEvcsApp = new KebaEvcs(this.componentManger, getComponentContext("App.Evcs.Keba"), this.cm, this.componentUtil); this.awattarApp = new AwattarHourly(this.componentManger, getComponentContext("App.TimeVariablePrice.Awattar"), @@ -237,18 +260,28 @@ public void beforeEach() throws Exception { this.stromdao = new StromdaoCorrently(this.componentManger, getComponentContext("App.TimeVariablePrice.Stromdao"), this.cm, this.componentUtil); + AggregateTask.ComponentAggregateTask componentTask = new ComponentAggregateTaskImpl(this.componentManger); + AggregateTask.SchedulerAggregateTask schedulerTask = new SchedulerAggregateTaskImpl(componentTask, + this.componentUtil); + AggregateTask.StaticIpAggregateTask staticIpTask = new StaticIpAggregateTaskImpl(this.componentUtil); + + var appManagerAppHelper = new AppManagerAppHelperImpl(this.componentManger, this.componentUtil, this.validator, + componentTask, schedulerTask, staticIpTask); + this.sut = new AppManagerImpl(); new ComponentTest(this.sut) // .addReference("cm", this.cm) // .addReference("componentManager", this.componentManger) // + .addReference("appHelper", appManagerAppHelper) // .addReference("availableApps", - ImmutableList.of(this.homeApp, this.kebaEvcsApp, this.awattarApp, this.stromdao)) // + ImmutableList.of(this.homeApp, this.gridOptimizedCharge, this.selfConsumptionOptimization, + this.kebaEvcsApp, this.awattarApp, this.stromdao)) // .activate(MyConfig.create() // .setApps(JsonUtils.buildJsonArray() // .add(JsonUtils.buildJsonObject() // .addProperty("appId", "App.FENECON.Home") // .addProperty("alias", "FENECON Home") // - .addProperty("instanceId", "ef13f394-1a3c-43ed-b726-ef1efaf23fdf") // + .addProperty("instanceId", UUID.randomUUID().toString()) // .add("properties", JsonUtils.buildJsonObject() // .addProperty("SAFETY_COUNTRY", safetyCountry) // .addProperty("MAX_FEED_IN_POWER", maxFeedInPower) // @@ -259,6 +292,24 @@ public void beforeEach() throws Exception { .addProperty("EMERGENCY_RESERVE_ENABLED", emergencyReserveEnabled) // .build()) // .build()) // + .add(JsonUtils.buildJsonObject() // + .addProperty("appId", "App.PvSelfConsumption.GridOptimizedCharge") // + .addProperty("alias", "") // + .addProperty("instanceId", UUID.randomUUID().toString()) // + .add("properties", JsonUtils.buildJsonObject() // + .addProperty("SELL_TO_GRID_LIMIT_ENABLED", true) // + .addProperty("MAXIMUM_SELL_TO_GRID_POWER", maxFeedInPower) // + .build()) // + .build()) + .add(JsonUtils.buildJsonObject() // + .addProperty("appId", "App.PvSelfConsumption.SelfConsumptionOptimization") // + .addProperty("alias", "") // + .addProperty("instanceId", UUID.randomUUID().toString()) // + .add("properties", JsonUtils.buildJsonObject() // + .addProperty("ESS_ID", essId) // + .addProperty("METER_ID", meterId) // + .build()) // + .build()) .build().toString()) // .build()); } @@ -268,7 +319,7 @@ public void testAppValidateWorker() throws OpenemsException, Exception { var worker = new AppValidateWorker(this.sut); worker.validateApps(); - assertEquals(this.sut.instantiatedApps.size(), 1); + assertEquals(this.sut.instantiatedApps.size(), 3); // should not have found defective Apps for (Entry entry : worker.defectiveApps.entrySet()) { @@ -281,15 +332,6 @@ public void testGetInstantiatedApps() { this.sut.getInstantiatedApps().add(null); } - @Test - public void testGetReplaceableComponentIds() throws Exception { - var replaceableIds = this.sut.getReplaceableComponentIds(this.kebaEvcsApp, JsonUtils.buildJsonObject().build()); - - assertEquals(replaceableIds.size(), 2); - assertEquals("EVCS_ID", replaceableIds.get("evcs0")); - assertEquals("CTRL_EVCS_ID", replaceableIds.get("ctrlEvcs0")); - } - @Test public void testFindAppById() { assertEquals(this.homeApp, this.sut.findAppById("App.FENECON.Home")); @@ -297,38 +339,38 @@ public void testFindAppById() { @Test public void testCheckCardinalitySingle() throws Exception { - var checkable = new CheckCardinality(this.sut); - checkable.setProperties(new Validator.MapBuilder<>(new TreeMap()) // + var checkable = new CheckCardinality(this.sut, getComponentContext(CheckCardinality.COMPONENT_NAME)); + checkable.setProperties(new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("openemsApp", this.homeApp) // .build()); assertFalse(checkable.check()); - assertNotNull(checkable.getErrorMessage()); + assertNotNull(checkable.getErrorMessage(Language.DEFAULT)); } @Test public void testCheckCardinalityMultiple() throws Exception { this.sut.instantiatedApps.add(new OpenemsAppInstance(this.kebaEvcsApp.getAppId(), "alias", UUID.randomUUID(), - JsonUtils.buildJsonObject().build())); + JsonUtils.buildJsonObject().build(), null)); this.sut.instantiatedApps.add(new OpenemsAppInstance(this.kebaEvcsApp.getAppId(), "alias", UUID.randomUUID(), - JsonUtils.buildJsonObject().build())); - var checkable = new CheckCardinality(this.sut); - checkable.setProperties(new Validator.MapBuilder<>(new TreeMap()) // + JsonUtils.buildJsonObject().build(), null)); + var checkable = new CheckCardinality(this.sut, getComponentContext(CheckCardinality.COMPONENT_NAME)); + checkable.setProperties(new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("openemsApp", this.kebaEvcsApp) // .build()); assertTrue(checkable.check()); - assertNull(checkable.getErrorMessage()); + assertNull(checkable.getErrorMessage(Language.DEFAULT)); } @Test public void testCheckCardinalitySingleInCategorie() throws Exception { this.sut.instantiatedApps.add(new OpenemsAppInstance(this.awattarApp.getAppId(), "alias", UUID.randomUUID(), - JsonUtils.buildJsonObject().build())); - var checkable = new CheckCardinality(this.sut); - checkable.setProperties(new Validator.MapBuilder<>(new TreeMap()) // + JsonUtils.buildJsonObject().build(), null)); + var checkable = new CheckCardinality(this.sut, getComponentContext(CheckCardinality.COMPONENT_NAME)); + checkable.setProperties(new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("openemsApp", this.stromdao) // .build()); assertFalse(checkable.check()); - assertNotNull(checkable.getErrorMessage()); + assertNotNull(checkable.getErrorMessage(Language.DEFAULT)); } private static ComponentContext getComponentContext(String appId) { diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyValidator.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyValidator.java new file mode 100644 index 00000000000..65e59feed2f --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyValidator.java @@ -0,0 +1,43 @@ +package io.openems.edge.core.appmanager; + +import java.util.ArrayList; +import java.util.List; + +import io.openems.common.session.Language; +import io.openems.edge.core.appmanager.validator.Checkable; +import io.openems.edge.core.appmanager.validator.Validator; +import io.openems.edge.core.appmanager.validator.ValidatorConfig.CheckableConfig; + +public class DummyValidator implements Validator { + + private List checkables; + + @Override + public List getErrorMessages(List checkableConfigs, Language language, + boolean returnImmediate) { + var errors = new ArrayList(); + for (var check : checkableConfigs) { + var checkable = this.findCheckableByName(check.checkableComponentName); + checkable.setProperties(check.properties); + if (!checkable.check()) { + errors.add(checkable.getErrorMessage(language)); + return errors; + } + + } + return errors; + } + + private Checkable findCheckableByName(String name) { + return this.checkables.stream().filter(c -> c.getComponentName().equals(name)).findAny().get(); + } + + public void setCheckables(List checkables) { + this.checkables = checkables; + } + + public List getCheckables() { + return this.checkables; + } + +} diff --git a/ui/src/app/edge/settings/app/index.component.html b/ui/src/app/edge/settings/app/index.component.html index 83e7bdfead4..6164e16233f 100644 --- a/ui/src/app/edge/settings/app/index.component.html +++ b/ui/src/app/edge/settings/app/index.component.html @@ -23,8 +23,7 @@ - Der App-Manager befindet sich aktuell in einer ersten Testversion. Falls nicht alle - Apps angezeigt werden, muss evtl. die FEMS Version geupdatet werden. + Edge.Config.App.header @@ -87,12 +86,12 @@

- errorCompatibleMessages:
+ Edge.Config.App.errorCompatible
- {{ message }}
- errorInstallableMessages:
+ Edge.Config.App.errorInstallable
- {{ message }}
@@ -133,12 +132,12 @@

- errorCompatibleMessages:
+ Edge.Config.App.errorCompatible
- {{ message }}
- errorInstallableMessages:
+ Edge.Config.App.errorInstallable
- {{ message }}
diff --git a/ui/src/app/edge/settings/app/index.component.ts b/ui/src/app/edge/settings/app/index.component.ts index 4650b5deb03..ce41050417d 100644 --- a/ui/src/app/edge/settings/app/index.component.ts +++ b/ui/src/app/edge/settings/app/index.component.ts @@ -1,5 +1,7 @@ import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { timeout } from 'rxjs/operators'; import { ComponentJsonApiRequest } from 'src/app/shared/jsonrpc/request/componentJsonApiRequest'; import { environment } from 'src/environments'; import { Service, Websocket } from '../../../shared/shared'; @@ -14,25 +16,31 @@ export class IndexComponent { private static readonly SELECTOR = "appIndex"; public readonly spinnerId: string = IndexComponent.SELECTOR; + /** + * e. g. if more than 4 apps are in a list the apps are displayed in their categories + */ + private static readonly MAX_APPS_IN_LIST = 4; + public apps: GetApps.App[] = []; - public installedApps: AppList = { name: "Installiert", appCategories: [] }; - public availableApps: AppList = { name: "Verfügbar", appCategories: [] }; + public installedApps: AppList = { name: this.translate.instant('Edge.Config.App.installed'), appCategories: [] }; + public availableApps: AppList = { name: this.translate.instant('Edge.Config.App.available'), appCategories: [] }; // TODO incompatible apps should not be shown in the future - public incompatibleApps: AppList = { name: "Incompatible", appCategories: [] }; + public incompatibleApps: AppList = { name: this.translate.instant('Edge.Config.App.incompatible'), appCategories: [] }; public appLists: AppList[] = [this.installedApps, this.availableApps, this.incompatibleApps]; public categories = []; - constructor( + public constructor( private route: ActivatedRoute, private service: Service, private websocket: Websocket, + private translate: TranslateService, ) { } - ionViewWillEnter() { + private ionViewWillEnter() { // gets always called when entering the page this.init() } @@ -62,7 +70,6 @@ export class IndexComponent { this.categories.push({ val: category, isChecked: true }) } }); - }) this.updateSelection(null) @@ -74,7 +81,11 @@ export class IndexComponent { }); } - public updateSelection(event) { + /** + * Updates the slected categories. + * @param event the event of a click on a 'ion-fab-list' to stop it from closing + */ + private updateSelection(event) { if (event != null) { event.stopPropagation() } @@ -91,7 +102,6 @@ export class IndexComponent { } return true }) - }) sortedApps.forEach(a => { @@ -122,7 +132,7 @@ export class IndexComponent { } private showCategories(app: AppList) { - return this.sum(app) > 4 + return this.sum(app) > IndexComponent.MAX_APPS_IN_LIST } private isEmpty(app: AppList) { diff --git a/ui/src/app/edge/settings/app/install.component.html b/ui/src/app/edge/settings/app/install.component.html index f0dc9f0f4e0..3508efec352 100644 --- a/ui/src/app/edge/settings/app/install.component.html +++ b/ui/src/app/edge/settings/app/install.component.html @@ -13,7 +13,8 @@ - App installieren + + Edge.Config.App.createApp diff --git a/ui/src/app/edge/settings/app/install.component.ts b/ui/src/app/edge/settings/app/install.component.ts index c6544f5b5e8..f48cea893af 100644 --- a/ui/src/app/edge/settings/app/install.component.ts +++ b/ui/src/app/edge/settings/app/install.component.ts @@ -25,7 +25,7 @@ export class InstallAppComponent implements OnInit { private edge: Edge = null; private isInstalling: boolean; - constructor( + public constructor( private route: ActivatedRoute, protected utils: Utils, private websocket: Websocket, @@ -33,7 +33,7 @@ export class InstallAppComponent implements OnInit { ) { } - ngOnInit() { + public ngOnInit() { this.service.startSpinner(this.spinnerId); let appId = this.route.snapshot.params["appId"]; this.appId = appId; @@ -83,12 +83,23 @@ export class InstallAppComponent implements OnInit { properties: clonedFields }) })).then(response => { + let result = (response as AddAppInstance.Response).result + + if (result.instance) { + result.instanceId = result.instance.instanceId + this.model = result.instance.properties + } + if (result.warnings && result.warnings.length > 0) { + this.service.toast(result.warnings.join(";"), 'warning') + } else { + this.service.toast("Successfully installed App", 'success'); + } + this.form.markAsPristine(); - this.isInstalling = false - this.service.toast("Successfully installed App", 'success'); }).catch(reason => { - this.isInstalling = false this.service.toast("Error installing App:" + reason.error.message, 'danger'); + }).finally(() => { + this.isInstalling = false }); } diff --git a/ui/src/app/edge/settings/app/jsonrpc/addAppInstance.ts b/ui/src/app/edge/settings/app/jsonrpc/addAppInstance.ts index e272cedda5e..74cef3fd4f6 100644 --- a/ui/src/app/edge/settings/app/jsonrpc/addAppInstance.ts +++ b/ui/src/app/edge/settings/app/jsonrpc/addAppInstance.ts @@ -1,4 +1,5 @@ import { JsonrpcRequest, JsonrpcResponseSuccess } from "../../../../shared/jsonrpc/base"; +import { GetAppInstances } from "./getAppInstances"; /** * Adds an OpenemsAppInstance. @@ -54,7 +55,9 @@ export namespace AddAppInstance { public constructor( public readonly id: string, public readonly result: { - instanceId: string + instanceId: string, + instance: GetAppInstances.AppInstance, + warnings: String[] } ) { super(id, result); diff --git a/ui/src/app/edge/settings/app/jsonrpc/updateAppInstance.ts b/ui/src/app/edge/settings/app/jsonrpc/updateAppInstance.ts index 7fc81036238..9c45d1a0863 100644 --- a/ui/src/app/edge/settings/app/jsonrpc/updateAppInstance.ts +++ b/ui/src/app/edge/settings/app/jsonrpc/updateAppInstance.ts @@ -1,4 +1,6 @@ -import { JsonrpcRequest } from "../../../../shared/jsonrpc/base"; +import { JsonrpcRequest, JsonrpcResponseSuccess } from "../../../../shared/jsonrpc/base"; +import { GetAppInstances } from "./getAppInstances"; +import { GetApps } from "./getApps"; /** * Updates an instance of an {@link OpenemsApp}. @@ -47,4 +49,18 @@ export namespace UpdateAppInstance { } } + + export class Response extends JsonrpcResponseSuccess { + + public constructor( + public readonly id: string, + public readonly result: { + instance: GetAppInstances.AppInstance, + warnings: String[] + } + ) { + super(id, result); + } + } + } \ No newline at end of file diff --git a/ui/src/app/edge/settings/app/single.component.html b/ui/src/app/edge/settings/app/single.component.html index 5ea9f902076..d6d148de60c 100644 --- a/ui/src/app/edge/settings/app/single.component.html +++ b/ui/src/app/edge/settings/app/single.component.html @@ -18,27 +18,29 @@
- App kaufen + Edge.Config.App.buyApp
App bearbeiten + [routerLink]="['../../update', app.appId]" translate>Edge.Config.App.modifyApp
App anlegen + [routerLink]="['../../install', app.appId]" translate>Edge.Config.App.createApp
- errorCompatibleMessages:
- - {{ message }}
+ Edge.Config.App.errorCompatible
+ - {{ message + }}
- errorInstallableMessages:
- - {{ message }}
+ Edge.Config.App.errorInstallable
+ - {{ message + }}
diff --git a/ui/src/app/edge/settings/app/single.component.ts b/ui/src/app/edge/settings/app/single.component.ts index b4af1833140..0eafc6c4961 100644 --- a/ui/src/app/edge/settings/app/single.component.ts +++ b/ui/src/app/edge/settings/app/single.component.ts @@ -31,7 +31,7 @@ export class SingleAppComponent implements OnInit { private edge: Edge = null; - constructor( + public constructor( private route: ActivatedRoute, protected utils: Utils, private websocket: Websocket, @@ -40,7 +40,7 @@ export class SingleAppComponent implements OnInit { ) { } - ngOnInit() { + public ngOnInit() { this.service.startSpinner(this.spinnerId); this.updateIsXL(); this.appId = this.route.snapshot.params["appId"]; @@ -82,7 +82,7 @@ export class SingleAppComponent implements OnInit { } @HostListener('window:resize', ['$event']) - onResize(event) { + private onResize(event) { this.updateIsXL(); } diff --git a/ui/src/app/edge/settings/app/update.component.html b/ui/src/app/edge/settings/app/update.component.html index ef6200a98b9..1a722b5ce2c 100644 --- a/ui/src/app/edge/settings/app/update.component.html +++ b/ui/src/app/edge/settings/app/update.component.html @@ -14,12 +14,12 @@ - App aktualisieren + [disabled]="(!((!varinstance.form.pristine) && (varinstance.form.valid))) || varinstance.isDeleting || varinstance.isUpdateting" + translate> Edge.Config.App.updateApp - App entfernen + [disabled]="varinstance.isDeleting || varinstance.isUpdateting" translate> + Edge.Config.App.deleteApp diff --git a/ui/src/app/edge/settings/app/update.component.ts b/ui/src/app/edge/settings/app/update.component.ts index d378bec0bfc..970439079c6 100644 --- a/ui/src/app/edge/settings/app/update.component.ts +++ b/ui/src/app/edge/settings/app/update.component.ts @@ -34,7 +34,7 @@ export class UpdateAppComponent implements OnInit { private appName: string; - constructor( + public constructor( private route: ActivatedRoute, protected utils: Utils, private websocket: Websocket, @@ -42,7 +42,7 @@ export class UpdateAppComponent implements OnInit { ) { } - ngOnInit() { + public ngOnInit() { this.service.startSpinner(this.spinnerId); let appId = this.route.snapshot.params["appId"]; this.service.setCurrentComponent("App " + appId, this.route).then(edge => { @@ -114,7 +114,15 @@ export class UpdateAppComponent implements OnInit { properties: clonedFields }) })).then(response => { - this.service.toast("Successfully updated App", 'success'); + var res = (response as UpdateAppInstance.Response); + + if (res.result.warnings && res.result.warnings.length > 0) { + this.service.toast(res.result.warnings.join(";"), 'warning'); + } else { + this.service.toast("Successfully updated App", 'success'); + } + instance.properties = res.result.instance.properties + instance.properties["ALIAS"] = res.result.instance.alias instance.isUpdateting = false }).catch(reason => { this.service.toast("Error updating App:" + reason.error.message, 'danger'); @@ -131,11 +139,12 @@ export class UpdateAppComponent implements OnInit { instanceId: instance.instanceId }) })).then(response => { - this.service.toast("Successfully deleted App", 'success'); this.instances.splice(this.instances.indexOf(instance), 1) + this.service.toast("Successfully deleted App", 'success'); }).catch(reason => { this.service.toast("Error deleting App:" + reason.error.message, 'danger'); - this.instances.splice(this.instances.indexOf(instance), 1) - }); + }).finally(() => { + instance.isDeleting = false + }) } } \ No newline at end of file diff --git a/ui/src/app/edge/settings/settings.component.html b/ui/src/app/edge/settings/settings.component.html index 5c67be5084f..42b7b06a13f 100644 --- a/ui/src/app/edge/settings/settings.component.html +++ b/ui/src/app/edge/settings/settings.component.html @@ -123,7 +123,7 @@ - {{ environment.edgeShortName }} Apps (Beta-Test) + {{ environment.edgeShortName }} App Center diff --git a/ui/src/app/shared/header/header.component.ts b/ui/src/app/shared/header/header.component.ts index 9e114d77fda..809baa445d1 100644 --- a/ui/src/app/shared/header/header.component.ts +++ b/ui/src/app/shared/header/header.component.ts @@ -113,6 +113,12 @@ export class HeaderComponent { if (file === 'live') { urlArray.pop(); } + + // fix url for App "settings/app/install" and "settings/app/update" + if (urlArray.slice(-3, -1).join('/') === "settings/app") { + urlArray.pop(); + } + // re-join the url backUrl = urlArray.join('/') || '/'; diff --git a/ui/src/app/shared/translate/cz.ts b/ui/src/app/shared/translate/cz.ts index e586482f692..660ca6b69ab 100644 --- a/ui/src/app/shared/translate/cz.ts +++ b/ui/src/app/shared/translate/cz.ts @@ -456,7 +456,20 @@ export const TRANSLATION = { temperatures: "Teplota buňky", insulation: "Izolace", } - } + }, + App: { + header: 'Správce aplikací je v současné době v první testovací verzi. Pokud se nezobrazují všechny aplikace, je možné, že bude třeba aktualizovat verzi FEMS.', + installed: 'Nainstalováno', + available: 'Dostupné na', + incompatible: 'Nekompatibilní', + buyApp: 'koupit aplikaci', + modifyApp: 'upravit aplikaci', + createApp: 'Instalace aplikace', + deleteApp: 'odstranit aplikaci', + updateApp: 'aktualizace aplikace', + errorInstallable: 'Chyby pÅ™i instalaci', + errorCompatible: 'Chyby kompatibility', + }, }, About: { build: "Aktuální verze", diff --git a/ui/src/app/shared/translate/de.ts b/ui/src/app/shared/translate/de.ts index 5bc3117e918..c96ab7f602d 100644 --- a/ui/src/app/shared/translate/de.ts +++ b/ui/src/app/shared/translate/de.ts @@ -472,6 +472,19 @@ export const TRANSLATION = { delay: 'Verzögerung [min]', save: 'Speichern', }, + App: { + header: 'Der App-Manager befindet sich aktuell in einer ersten Testversion. Falls nicht alle Apps angezeigt werden, muss evtl. die FEMS Version geupdatet werden.', + installed: 'Installiert', + available: 'Verfügbar', + incompatible: 'Inkompatibel', + buyApp: 'App kaufen', + modifyApp: 'App bearbeiten', + createApp: 'App installieren', + deleteApp: 'App entfernen', + updateApp: 'App aktualisieren', + errorInstallable: 'Installierungs fehler', + errorCompatible: 'Kompatibilitäts fehler', + }, } }, About: { diff --git a/ui/src/app/shared/translate/en.ts b/ui/src/app/shared/translate/en.ts index 47cd70b605e..9060b378d89 100644 --- a/ui/src/app/shared/translate/en.ts +++ b/ui/src/app/shared/translate/en.ts @@ -471,7 +471,20 @@ export const TRANSLATION = { activate: 'Activate', delay: 'Delay [min]', save: 'Save', - } + }, + App: { + header: 'The App Manager is currently in a first test version. If not all apps are displayed, the FEMS version may need to be updated.', + installed: 'Installed', + available: 'Available', + incompatible: 'Incompatible', + buyApp: 'Buy app', + modifyApp: 'Modify app', + createApp: 'Install app', + deleteApp: 'Delete app', + updateApp: 'Update app', + errorInstallable: 'Installation errors', + errorCompatible: 'Compatibility errors', + }, } }, About: { diff --git a/ui/src/app/shared/translate/es.ts b/ui/src/app/shared/translate/es.ts index 66b55145b27..c94e0050ac5 100644 --- a/ui/src/app/shared/translate/es.ts +++ b/ui/src/app/shared/translate/es.ts @@ -440,7 +440,20 @@ export const TRANSLATION = { activate: 'Activar', delay: 'Retraso [min]', save: 'Guardar', - } + }, + App: { + header: 'El App Manager se encuentra actualmente en una primera versión de prueba. Si no se muestran todas las aplicaciones, es posible que haya que actualizar la versión de FEMS.', + installed: 'Instalado', + available: 'Disponible', + incompatible: 'Incompatible', + buyApp: 'Comprar aplicación', + modifyApp: 'Modificar la aplicación', + createApp: 'Instalar la aplicación', + deleteApp: 'Eliminar la aplicación', + updateApp: 'Actualizar la aplicación', + errorInstallable: 'Errores de instalación', + errorCompatible: 'Errores de compatibilidad', + }, } }, About: { diff --git a/ui/src/app/shared/translate/fr.ts b/ui/src/app/shared/translate/fr.ts index 8417b15dade..dbab824ff88 100644 --- a/ui/src/app/shared/translate/fr.ts +++ b/ui/src/app/shared/translate/fr.ts @@ -442,7 +442,20 @@ export const TRANSLATION = { activate: 'Activer', delay: 'Retard [min]', save: 'Enregistrer', - } + }, + App: { + header: 'L\'App Manager est actuellement dans une première version de test. Si toutes les applications ne sont pas affichées, il est possible que la version FEMS doive être mise à jour.', + installed: 'Installé', + available: 'Disponible sur', + incompatible: 'Incompatibilité', + buyApp: 'Acheter l\'application', + modifyApp: 'Modifier l\'application', + createApp: 'Installer l\'application', + deleteApp: 'Supprimer l\'application', + updateApp: 'Mise à jour de l\'application', + errorInstallable: 'Erreurs d\'installation', + errorCompatible: 'Erreurs de compatibilité', + }, } }, About: { diff --git a/ui/src/app/shared/translate/nl.ts b/ui/src/app/shared/translate/nl.ts index 4524daedcab..d0854b7cccd 100644 --- a/ui/src/app/shared/translate/nl.ts +++ b/ui/src/app/shared/translate/nl.ts @@ -439,7 +439,20 @@ export const TRANSLATION = { activate: 'Activeer', delay: 'Vertraging [min]', save: 'Save', - } + }, + App: { + header: 'De App Manager bevindt zich momenteel in een eerste testversie. Als niet alle apps worden weergegeven, moet de FEMS-versie mogelijk worden bijgewerkt.', + installed: 'Geïnstalleerd', + available: 'Beschikbaar', + incompatible: 'Onverenigbaar', + buyApp: 'App kopen', + modifyApp: 'App wijzigen', + createApp: 'App installeren', + deleteApp: 'App verwijderen', + updateApp: 'App bijwerken', + errorInstallable: 'Installatiefouten', + errorCompatible: 'Compatibiliteitsfouten', + }, } }, About: {