Skip to content

Commit

Permalink
FEMS Backports 2025-02-02 (#2997)
Browse files Browse the repository at this point in the history
* UI
  * Change display color of currency axes
    - fix coloring of yAxes for currency
    - extending eslint rule [`no-unused-vars`](https://eslint.org/docs/latest/rules/no-unused-vars#ignorerestsiblings) with `ignoreRestSiblings`: destructuring throws otherwise an error for unused sibling
  * Adjust custom time range chart display
    - Adjust time-range picker in mode `Custom` to display only the months queried for monthly resolution, before if start and endpoint are in a different year, it would fill up both years to show 12 months per year
  * Fix searchbar request overload
    - reducing calls of `GetEdgesRequest` and waiting for last requests response
    - avoids showing every response
    - introduce 1 second `debounce`
  * Refactor heat pump history
    - Using `Time`-channels for bar charts
    - creating refactored standalone unit tested lazy loaded history for heatpump
    - sanitizing data for io time channels if they are not adding up to
      - 24h (1 day - month view) -> rounding if only one minute missing
      - 744h (31 day month - year view) ... -> rounding if only one hour missing
      - -> increasing `RegularStateTime` by the missing diff
    - Attention: There still could be some deviation due to OpenEMS Edge being offline during systemupdate
  * Improve styling
    * Remove unused imports in GetNetworkInfo and MennekesEvcs
  * Controller.IO.Heating.Room: add UI Live widget
    - Add UI Live Flat and Modal for `Controller.IO.Heating.Room`
    - This implementation could in future serve as a proof of concept for new JSCalendar configurations
  * EVCS Cluster: remove slides from modal
    - Because of display errors after recent Angular & Ionic update.

* Edge
  * Host: extend getIpAddresses Request
    * Extended jsonRpc request for getIpAddresses with more data for networkInterfaces and IP routing table
  * Mennekes Relase: Remove AppPermissions
    * Remove App Permissions from Mennekes for upcoming release
  * SunSpec: set correct OpenemsType for floating point values
    * See commit details for an example of the changes for a Fronius PV Inverter with SunSpec Block 113
    * Originally we tried to avoid floating point in favour of integer numbers to avoid very frequent sending of changed values. This has been solved in the meantime with a DEFAULT_PERSISTENCE_PRIORITY of VERY_LOW for SunSpec channels.
  * ModbusTcp Read-Only: add modified handling
    * Adds modified method to ModbusTcp Read-Only
  * AppCenter: fix bug where all array properties cant be modified
    - Modifying array properties results in an error "expected array length 1 not 3"
    - "fixed" by changing order of checks to first check if property can be modified and then check if they are the same value
    - TODO future if access check on a array based property happens it needs to be modified to handle array values
    - remove excessive log if component does not exist in `ComponentManager#getComponentProperties`
  * Energy Optimizer: fix possible NPE
    - This fixes
```
[_energy ] ERROR [s.common.worker.AbstractWorker] Worker error. NullPointerException: Cannot invoke "io.jenetics.Genotype.get(int)" because "bestGt" is null
java.lang.NullPointerException: Cannot invoke "io.jenetics.Genotype.get(int)" because "bestGt" is null
```
    - According to docs the Collector can indeed return `null`: https://github.com/jenetics/jenetics/blob/master/jenetics/src/main/java/io/jenetics/engine/EvolutionResult.java#L475-L476
  * ElectricityMeter: added calculateCurrentsFromActivePowerAndVoltage and calculatePhasesFromVoltage
    - ElectricityMeter enhancement for more easier integration of future EVCSs. Method may be useful for general ElectricityMeter implementations as well. That's why it was placed in ElectricityMeter and not in Evcs.
  * Windows Fix for IP Validation in Apps
    - IP Validation Regex is now Compatible with Windows again. (Currently when OpenEMS is run locally apps with IP Validation can't be installed)
  * GoodWeGridMeter: ElementToChannelConverter NP-Fix
    - Null check in element to channel converter
  * GoodWe: improve Battery Power Settings
    - Some of the Power settings for the goodwe were not applied correctly (Per default disabled, wrong scale factors, wrong register mapping cosPhiF != PU)
  * GoodWe: ignore impossible power values
    - The DcActivePower resgister of GoodWe is giving 20-40 watt values when the SoC is 0 or 100 and there is no real charge/discharge of the battery happening. This values will be ignored to avoid wrong energy values, as the energy is calculated based on the active power.
    - The same occurs when the battery is getting a 0 charge/discharge set point. If the SoC is between 0 and 100 it is also ignoring this low power values when the current EmsPowerMode is Charge/Discharge Bat & the EmsPowerSet is 0 W

---------

Co-authored-by: Stefan Feilmeier <[email protected]>
Co-authored-by: Lukas Rieger <[email protected]>
Co-authored-by: Johann Kaufmann <[email protected]>
Co-authored-by: Michael Grill <[email protected]>
Co-authored-by: Fabian Brandtner <[email protected]>
Co-authored-by: Sebastian Asen <[email protected]>
Co-authored-by: Christian Lehne <[email protected]>
  • Loading branch information
8 people authored Feb 2, 2025
1 parent 68be9f2 commit 306f230
Show file tree
Hide file tree
Showing 104 changed files with 1,866 additions and 797 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,14 @@ public ValuePoint(String name, String label, String description, ValuePoint.Type
this(name, label, description, type, mandatory, accessMode, unit, //
Doc.of(//
switch (type) {
case UINT16, ACC16, INT16, COUNT, INT32, PAD, // ignore
EUI48, FLOAT32 // avoid floating point numbers; FLOAT32 might not fit in INTEGER
case UINT16, ACC16, INT16, COUNT, INT32, PAD, EUI48 //
-> OpenemsType.INTEGER;
case ACC32, IPADDR, UINT32, UINT64, ACC64, INT64, IPV6ADDR, //
FLOAT64 // avoid floating point numbers
case ACC32, IPADDR, UINT32, UINT64, ACC64, INT64, IPV6ADDR //
-> OpenemsType.LONG;
case FLOAT32 //
-> OpenemsType.FLOAT;
case FLOAT64 //
-> OpenemsType.DOUBLE;
case STRING2, STRING4, STRING5, STRING6, STRING7, STRING8, STRING12, STRING16, STRING20,
STRING25, STRING32 //
-> OpenemsType.STRING;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import static io.openems.common.channel.ChannelCategory.OPENEMS_TYPE;
import static io.openems.common.channel.PersistencePriority.VERY_LOW;
import static io.openems.common.channel.Unit.AMPERE;
import static io.openems.common.types.OpenemsType.INTEGER;
import static io.openems.common.types.OpenemsType.FLOAT;
import static org.junit.Assert.assertEquals;

import org.junit.Test;
Expand All @@ -22,7 +22,7 @@ public void testChannelIdPoint() {
assertEquals(OPENEMS_TYPE, doc.getChannelCategory());
assertEquals(VERY_LOW, doc.getPersistencePriority());
assertEquals("Amps. AC Current", doc.getText());
assertEquals(INTEGER, doc.getType());
assertEquals(FLOAT, doc.getType());
assertEquals(AMPERE, doc.getUnit());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
Expand All @@ -20,6 +21,7 @@
import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory;

import io.openems.common.channel.AccessMode;
import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.exceptions.OpenemsException;
import io.openems.common.utils.FunctionUtils;
import io.openems.edge.common.channel.WriteChannel;
Expand Down Expand Up @@ -75,6 +77,13 @@ private void activate(ComponentContext context, Config config) throws ModbusExce
super.activate(context, this.cm, this.config);
}

@Modified
private void modified(ComponentContext context, Config config) throws OpenemsNamedException {
this.config = new TcpConfig(config.id(), config.alias(), config.enabled(), this.metaComponent,
config.component_ids(), 0 /* no timeout */, config.port(), config.maxConcurrentConnections());
super.modified(context, this.cm, this.config);
}

@Override
@Deactivate
protected void deactivate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,16 @@ AppDef<APP, PROP, PARAM> excludingIp() {
def -> def.setField(JsonFormlyUtil::buildInputFromNameable, (app, prop, l, param, f) -> {
try {
var ips = app.getHost().getSystemIPs();
final var exclusionPattern = ips.stream().map(ip -> ip.getHostAddress())//
.map(ip -> ip.replace(".", "\\.")) //
.collect(joining("|"));
if (ips.isEmpty()) {
f.setValidation(IP);
} else {
final var exclusionPattern = ips.stream().map(ip -> ip.getHostAddress())//
.map(ip -> ip.replace(".", "\\.")) //
.collect(joining("|"));

final var regex = "^(?!.*(?:" + exclusionPattern + ")$)" + PATTERN_INET4ADDRESS;
f.setValidation(regex, getTranslation(param.bundle(), "communication.excludingIp"));
f.setValidation("^(?!.*(?:" + exclusionPattern + ")$)" + PATTERN_INET4ADDRESS,
getTranslation(param.bundle(), "communication.excludingIp"));
}
} catch (OpenemsNamedException e) {
f.setValidation(IP);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import io.openems.common.function.ThrowingTriFunction;
import io.openems.common.oem.OpenemsEdgeOem;
import io.openems.common.session.Language;
import io.openems.common.session.Role;
import io.openems.common.types.EdgeConfig;
import io.openems.common.utils.JsonUtils;
import io.openems.edge.app.common.props.CommunicationProps;
Expand All @@ -36,7 +35,6 @@
import io.openems.edge.core.appmanager.OpenemsApp;
import io.openems.edge.core.appmanager.OpenemsAppCardinality;
import io.openems.edge.core.appmanager.OpenemsAppCategory;
import io.openems.edge.core.appmanager.OpenemsAppPermissions;
import io.openems.edge.core.appmanager.Type;
import io.openems.edge.core.appmanager.Type.Parameter;
import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter;
Expand Down Expand Up @@ -167,11 +165,4 @@ protected Property[] propertyValues() {
return Property.values();
}

@Override
public OpenemsAppPermissions getAppPermissions() {
return OpenemsAppPermissions.create()//
.setCanDelete(Role.ADMIN)//
.setCanSee(Role.ADMIN)//
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1009,15 +1009,19 @@ public UpdateAppInstance.Response handleUpdateAppInstanceRequest(User user, Upda
}
final var notAllowedProperties = props.keySet().stream()//
.filter(key -> {
if (restOfProps.has(key)) {

return (!props.get(key).getAsString().equals(restOfProps.get(key).getAsString()));
}
return false;
}).filter(key -> {
final var canEdit = app.assertCanEdit(key, user);
return !canEdit;
}).collect(Collectors.joining(", "));
}) //
.filter(key -> {
final var element = restOfProps.get(key);
if (element == null) {
return false;
}

// TODO special handling for arrays
return (!props.get(key).getAsString().equals(restOfProps.get(key).getAsString()));
}) //
.collect(Collectors.joining(", "));
if (notAllowedProperties.length() > 0) {
throw new OpenemsException("User is not allowed to edit " + notAllowedProperties + "!");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ public Map<String, Object> getComponentProperties(String componentId) {
try {
config = this.getExistingConfigForId(componentId);
} catch (OpenemsNamedException e) {
e.printStackTrace();
return emptyMap();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@
import io.openems.edge.core.host.jsonrpc.ExecuteSystemCommandRequest;
import io.openems.edge.core.host.jsonrpc.ExecuteSystemRestartRequest;
import io.openems.edge.core.host.jsonrpc.ExecuteSystemUpdateRequest;
import io.openems.edge.core.host.jsonrpc.GetIpAddresses;
import io.openems.edge.core.host.jsonrpc.GetIpAddresses.Response;
import io.openems.edge.core.host.jsonrpc.GetNetworkInfo;
import io.openems.edge.core.host.jsonrpc.GetNetworkConfigRequest;
import io.openems.edge.core.host.jsonrpc.GetNetworkConfigResponse;
import io.openems.edge.core.host.jsonrpc.GetSystemUpdateStateRequest;
Expand Down Expand Up @@ -188,13 +187,13 @@ public void buildJsonApiRoutes(JsonApiBuilder builder) {
ExecuteSystemRestartRequest.from(call.getRequest())).get();
});

builder.handleRequest(new GetIpAddresses(), endpoint -> {
builder.handleRequest(new GetNetworkInfo(), endpoint -> {
endpoint.setDescription("""
Gets the current ip addresses.
Gets the networkinfo.
""".stripIndent());

endpoint.setGuards(EdgeGuards.roleIsAtleast(Role.OWNER));
}, call -> new Response(this.getSystemIPs()));
}, call -> this.operatingSystem.getNetworkInfo());

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import io.openems.edge.core.host.jsonrpc.ExecuteSystemCommandResponse;
import io.openems.edge.core.host.jsonrpc.ExecuteSystemRestartRequest;
import io.openems.edge.core.host.jsonrpc.ExecuteSystemRestartResponse;
import io.openems.edge.core.host.jsonrpc.GetNetworkInfo;
import io.openems.edge.core.host.jsonrpc.SetNetworkConfigRequest;

public interface OperatingSystem {
Expand Down Expand Up @@ -71,6 +72,14 @@ public CompletableFuture<? extends JsonrpcResponseSuccess> handleExecuteSystemRe
*/
public List<Inet4Address> getSystemIPs() throws OpenemsNamedException;

/**
* Gets Network Info.
*
* @return Response of GetIpAddresses
* @throws OpenemsNamedException on error
*/
public GetNetworkInfo.Response getNetworkInfo() throws OpenemsNamedException;

/**
* Gets the current operating system version.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.openems.edge.core.host;

import static io.openems.common.jsonrpc.serialization.JsonSerializerUtil.jsonObjectSerializer;
import static io.openems.common.utils.FunctionUtils.doNothing;
import static java.lang.Runtime.getRuntime;
import static java.util.concurrent.CompletableFuture.runAsync;
Expand Down Expand Up @@ -33,17 +34,19 @@
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.exceptions.OpenemsException;
import io.openems.common.function.ThrowingConsumer;
import io.openems.common.jsonrpc.serialization.JsonSerializer;
import io.openems.common.types.ConfigurationProperty;
import io.openems.common.utils.InetAddressUtils;
import io.openems.common.utils.JsonUtils;
Expand All @@ -56,6 +59,9 @@
import io.openems.edge.core.host.jsonrpc.ExecuteSystemCommandResponse.SystemCommandResponse;
import io.openems.edge.core.host.jsonrpc.ExecuteSystemRestartRequest;
import io.openems.edge.core.host.jsonrpc.ExecuteSystemRestartResponse;
import io.openems.edge.core.host.jsonrpc.GetNetworkInfo;
import io.openems.edge.core.host.jsonrpc.GetNetworkInfo.NetworkInfoWrapper;
import io.openems.edge.core.host.jsonrpc.GetNetworkInfo.Route;
import io.openems.edge.core.host.jsonrpc.SetNetworkConfigRequest;

/**
Expand Down Expand Up @@ -581,50 +587,122 @@ protected static <A> NetworkInterface<A> parseSystemdNetworkdConfigurationFile(L

@Override
public List<Inet4Address> getSystemIPs() throws OpenemsNamedException {
var req = ExecuteSystemCommandRequest.withoutAuthentication("ip -j -4 address show", false, 5);
var reqIpShow = ExecuteSystemCommandRequest.withoutAuthentication("ip -j -4 address show", false, 5);
try {
var result = this.handleExecuteSystemCommandRequest(req).get().getResult().toString();
return parseIpJson(result);
var resultIpShow = this.handleExecuteSystemCommandRequest(reqIpShow).get().getResult().toString();
return parseShowJson(resultIpShow).stream().flatMap(t -> t.ips().stream().map(d -> d.getInet4Address()))
.toList();
} catch (InterruptedException | ExecutionException e) {
return Collections.emptyList();
}

}

/**
* Parses the json returned by ip address get command.
*
* @param result the json to be parsed
* @return a list of parsed ips
* @throws OpenemsNamedException on error
*/
protected static List<Inet4Address> parseIpJson(String result) throws OpenemsNamedException {
final var stdout = JsonUtils.getAsJsonArray(JsonUtils.getAsJsonObject(JsonUtils.parse(result)), "stdout");
@Override
public GetNetworkInfo.Response getNetworkInfo() throws OpenemsNamedException {
var reqIpShow = ExecuteSystemCommandRequest.withoutAuthentication("ip -j -4 address show", false, 5);
var reqIpRoute = ExecuteSystemCommandRequest.withoutAuthentication("ip -j route", false, 5);
try {
var resultIpShow = this.handleExecuteSystemCommandRequest(reqIpShow).get().getResult().toString();
var resultIpRoute = this.handleExecuteSystemCommandRequest(reqIpRoute).get().getResult().toString();
return new GetNetworkInfo.Response(parseShowJson(resultIpShow), parseRouteJson(resultIpRoute));
} catch (InterruptedException | ExecutionException e) {
return new GetNetworkInfo.Response(Collections.emptyList(), Collections.emptyList());
}

}

protected static List<JsonObject> parseIpJson(String json) throws OpenemsNamedException {
final var stdout = JsonUtils.getAsJsonArray(JsonUtils.getAsJsonObject(JsonUtils.parse(json)), "stdout");
final var networkData = stdout.get(0).getAsString();
final var networkDataJson = JsonUtils.parseOptional(networkData);
if (networkDataJson.isPresent() && networkDataJson.get().isJsonArray()) {
final var networkInterfaces = JsonUtils.getAsJsonArray(JsonUtils.parse(networkData));

if (networkData.startsWith("[")) {
return networkInterfaces.asList().stream().map(JsonElement::getAsJsonObject)
.map(interfaceObject -> interfaceObject.getAsJsonArray("addr_info"))
.flatMap(addrInfoArray -> StreamSupport.stream(addrInfoArray.spliterator(), false))
.map(JsonElement::getAsJsonObject)
.filter(addrInfoObject -> "inet".equals(addrInfoObject.get("family").getAsString()))
.map(addrInfoObject -> addrInfoObject.get("local").getAsString()) //
.<Inet4Address>mapMulti((t, u) -> {
try {
u.accept((Inet4Address) Inet4Address.getByName(t));
} catch (UnknownHostException e) {
// do nothing
}
}) //
.toList();//
return JsonUtils.stream(networkInterfaces)//
.map(JsonElement::getAsJsonObject)//
.toList();
}
}

return Collections.emptyList();
}

protected static List<Route> parseRouteJson(String routeJson) throws OpenemsNamedException {
final var networkData = parseIpJson(routeJson);
if (networkData == null) {
return Collections.emptyList();
}
return networkData.stream().map(t -> routeSerializer().deserialize(t)).toList();
}

private static JsonSerializer<GetNetworkInfo.Route> routeSerializer() {
return jsonObjectSerializer(GetNetworkInfo.Route.class, json -> {
Inet4Address prefsrc;
try {
// TODO: use inet4 method
prefsrc = (Inet4Address) Inet4Address.getByName(json.getString("prefsrc"));
} catch (UnknownHostException e) {
prefsrc = null;
}
return new GetNetworkInfo.Route(//
json.getString("dst"), //
json.getString("dev"), //
json.getString("protocol"), //
// TODO: use orElse in JsonPath once available
JsonUtils.getAsOptionalString(json.get(), "scope").orElse("link"), //
prefsrc, //
// TODO: use orElse in JsonPath once available and int method
JsonUtils.getAsOptionalInt(json.get(), "metric").orElse(DEFAULT_METRIC));
}, obj -> {
return JsonUtils.buildJsonObject() //
.addProperty("dst", obj.dst())//
.addProperty("dev", obj.dev())//
.addProperty("protocol", obj.protocol())//
.addProperty("scope", obj.scope())//
.addProperty("prefsrc", obj.prefsrc().getHostAddress())//
.addProperty("metric", obj.metric())//
.build();
});
}

/**
* Parses the json returned by ip address get command.
*
* @param resultIpShow the json to be parsed
* @return a list of parsed ips
* @throws OpenemsNamedException on error
*/
protected static List<NetworkInfoWrapper> parseShowJson(String resultIpShow) throws OpenemsNamedException {
final var networkInterfaces = parseIpJson(resultIpShow);
if (networkInterfaces == null) {
return Collections.emptyList();
}

final var networkDataRaw = networkInterfaces.stream()
.collect(Collectors.toMap(t -> t.get("ifname").getAsString(), interfaceObject -> {
var addrInfoArray = interfaceObject.getAsJsonArray("addr_info");
return JsonUtils.stream(addrInfoArray)//
.map(JsonElement::getAsJsonObject)//
.filter(addrInfoObject -> "inet".equals(addrInfoObject.get("family").getAsString())) //
.toList(); //
}));
return networkDataRaw.entrySet().stream().map(entry -> {
var ipsForKey = entry.getValue().stream().<Inet4AddressWithSubnetmask>mapMulti((t, u) -> {
try {
Inet4Address i4Address = (Inet4Address) Inet4Address.getByName(t.get("local").getAsString());
int subnetmask = t.get("prefixlen").getAsInt();
String family = t.get("family").getAsString();
u.accept(new Inet4AddressWithSubnetmask(family, i4Address, subnetmask));
} catch (Exception e) {
// do nothing
}
}).toList();
return new NetworkInfoWrapper(entry.getKey(), ipsForKey);
}).toList();

}

@Override
public CompletableFuture<String> getOperatingSystemVersion() {
final var sc = new SystemCommand(//
Expand Down
Loading

0 comments on commit 306f230

Please sign in to comment.