diff --git a/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java b/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java index cd0a08830ca..7bca125287b 100644 --- a/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java +++ b/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java @@ -71,6 +71,7 @@ public SystemUpdateParams getSystemUpdateParams() { .put("App.TimeOfUseTariff.Hassfurt", "") // .put("App.TimeOfUseTariff.RabotCharge", "") // .put("App.TimeOfUseTariff.Stromdao", "") // + .put("App.TimeOfUseTariff.Swisspower", "") // .put("App.TimeOfUseTariff.Tibber", "") // .put("App.Api.ModbusTcp.ReadOnly", "") // .put("App.Api.ModbusTcp.ReadWrite", "") // diff --git a/io.openems.common/src/io/openems/common/oem/OpenemsEdgeOem.java b/io.openems.common/src/io/openems/common/oem/OpenemsEdgeOem.java index 5f768f896f3..84dd61fb86f 100644 --- a/io.openems.common/src/io/openems/common/oem/OpenemsEdgeOem.java +++ b/io.openems.common/src/io/openems/common/oem/OpenemsEdgeOem.java @@ -133,13 +133,4 @@ public default String getEntsoeToken() { return null; } - /** - * Gets the OEM Access-Key for Exchangerate.host (used by - * TimeOfUseTariff.ENTSO-E). - * - * @return the value - */ - public default String getExchangeRateAccesskey() { - return null; - } } diff --git a/io.openems.common/src/io/openems/common/test/AbstractComponentConfig.java b/io.openems.common/src/io/openems/common/test/AbstractComponentConfig.java index af56ff73a01..c8f81ad2338 100644 --- a/io.openems.common/src/io/openems/common/test/AbstractComponentConfig.java +++ b/io.openems.common/src/io/openems/common/test/AbstractComponentConfig.java @@ -1,5 +1,7 @@ package io.openems.common.test; +import static io.openems.common.utils.ReflectionUtils.invokeMethodViaReflection; + import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -110,7 +112,7 @@ public Dictionary getAsProperties() } var key = method.getName().replace("_", "."); - var value = method.invoke(this); + var value = invokeMethodViaReflection(this, method); if (value == null) { throw new IllegalArgumentException("Configuration for [" + key + "] is null"); } diff --git a/io.openems.common/src/io/openems/common/utils/ReflectionUtils.java b/io.openems.common/src/io/openems/common/utils/ReflectionUtils.java index fc88d47a09c..ef924855595 100644 --- a/io.openems.common/src/io/openems/common/utils/ReflectionUtils.java +++ b/io.openems.common/src/io/openems/common/utils/ReflectionUtils.java @@ -1,21 +1,134 @@ package io.openems.common.utils; -import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import io.openems.common.function.ThrowingRunnable; +import io.openems.common.function.ThrowingSupplier; public class ReflectionUtils { + public static class ReflectionException extends RuntimeException { + private static final long serialVersionUID = -8001364348945297741L; + + protected static ReflectionException from(Exception e) { + return new ReflectionException(e.getClass().getSimpleName() + ": " + e.getMessage()); + } + + public ReflectionException(String message) { + super(message); + } + } + private ReflectionUtils() { // no instance needed } + protected static void callGuarded(ThrowingRunnable runnable) throws ReflectionException { + try { + runnable.run(); + } catch (Exception e) { + throw ReflectionException.from(e); + } + } + + protected static T callGuarded(ThrowingSupplier supplier) throws ReflectionException { + try { + return supplier.get(); + } catch (Exception e) { + throw ReflectionException.from(e); + } + } + + /** + * Sets the value of a Field via Java Reflection. + * + * @param object the target object + * @param memberName the name the declared field + * @param value the value to be set + * @throws Exception on error + */ + public static void setAttributeViaReflection(Object object, String memberName, Object value) + throws ReflectionException { + var field = getField(object.getClass(), memberName); + callGuarded(() -> field.set(object, value)); + } + + /** + * Sets the value of a static Field via Java Reflection. + * + * @param clazz the {@link Class} + * @param memberName the name the declared field + * @param value the value to be set + * @throws Exception on error + */ + public static void setStaticAttributeViaReflection(Class clazz, String memberName, Object value) + throws ReflectionException { + var field = getField(clazz, memberName); + callGuarded(() -> field.set(null, value)); + } + + /** + * Gets the value of a Field via Java Reflection. + * + * @param the type of the value + * @param object the target object + * @param memberName the name the declared field + * @return the value + * @throws Exception on error + */ @SuppressWarnings("unchecked") - public static boolean setAttribute(Class clazz, T object, String memberName, Object value) - throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + public static T getValueViaReflection(Object object, String memberName) throws ReflectionException { + var field = getField(object.getClass(), memberName); + return (T) callGuarded(() -> field.get(object)); + } + + /** + * Invokes a {@link Method} that takes no arguments via Java Reflection. + * + * @param the type of the result + * @param object the target object + * @param memberName the name of the method + * @return the result of the method + * @throws Exception on error + */ + public static T invokeMethodWithoutArgumentsViaReflection(Object object, String memberName) + throws ReflectionException { + var method = callGuarded(() -> object.getClass().getDeclaredMethod(memberName)); + return invokeMethodViaReflection(object, method); + } + + /** + * Invokes a {@link Method} via Java Reflection. + * + * @param the type of the result + * @param object the target object + * @param method the {@link Method} + * @param args the arguments to be set + * @return the result of the method + * @throws Exception on error + */ + @SuppressWarnings("unchecked") + public static T invokeMethodViaReflection(Object object, Method method, Object... args) + throws ReflectionException { + method.setAccessible(true); + return (T) callGuarded(() -> method.invoke(object, args)); + } + + /** + * Gets the {@link Class#getDeclaredField(String)} in the given {@link Class} or + * any of its superclasses. + * + * @param clazz the given {@link Class} + * @param memberName the name of the declared field + * @return a {@link Field} + * @throws ReflectionException if there is no such field + */ + public static Field getField(Class clazz, String memberName) throws ReflectionException { try { var field = clazz.getDeclaredField(memberName); field.setAccessible(true); - field.set(object, value); - return true; + return field; } catch (NoSuchFieldException e) { // Ignore. } @@ -23,9 +136,8 @@ public static boolean setAttribute(Class clazz, T object, Strin // classes. Class parent = clazz.getSuperclass(); if (parent == null) { - return false; // reached 'java.lang.Object' + throw new ReflectionException("Reached java.lang.Object"); } - return setAttribute((Class) parent, object, memberName, value); + return getField(parent, memberName); } - } diff --git a/io.openems.common/src/io/openems/common/utils/XmlUtils.java b/io.openems.common/src/io/openems/common/utils/XmlUtils.java index b921670e906..0a81ca749b8 100644 --- a/io.openems.common/src/io/openems/common/utils/XmlUtils.java +++ b/io.openems.common/src/io/openems/common/utils/XmlUtils.java @@ -1,5 +1,7 @@ package io.openems.common.utils; +import java.io.IOException; +import java.io.StringReader; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -7,9 +9,15 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + import org.w3c.dom.DOMException; +import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; import io.openems.common.exceptions.OpenemsError; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; @@ -254,4 +262,26 @@ public static Stream stream(final Node node) { var childNodes = node.getChildNodes(); return IntStream.range(0, childNodes.getLength()).boxed().map(childNodes::item); } + + /** + * Parses the provided XML string and returns the root {@link Element} of the + * XML document. + * + * @param xml the XML string to parse + * @return the root {@link Element} of the parsed XML document + * @throws ParserConfigurationException if a DocumentBuilder cannot be created + * which satisfies the configuration + * requested + * @throws SAXException if any parse errors occur while + * processing the XML + * @throws IOException if an I/O error occurs during parsing + */ + public static Element getXmlRootDocument(String xml) + throws ParserConfigurationException, SAXException, IOException { + var dbFactory = DocumentBuilderFactory.newInstance(); + var dBuilder = dbFactory.newDocumentBuilder(); + var is = new InputSource(new StringReader(xml)); + var doc = dBuilder.parse(is); + return doc.getDocumentElement(); + } } diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index 4dfeab59531..218d2542807 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -190,6 +190,7 @@ bnd.identity;id='io.openems.edge.timeofusetariff.groupe',\ bnd.identity;id='io.openems.edge.timeofusetariff.hassfurt',\ bnd.identity;id='io.openems.edge.timeofusetariff.rabotcharge',\ + bnd.identity;id='io.openems.edge.timeofusetariff.swisspower',\ bnd.identity;id='io.openems.edge.timeofusetariff.tibber',\ -runbundles: \ @@ -373,6 +374,7 @@ io.openems.edge.timeofusetariff.groupe;version=snapshot,\ io.openems.edge.timeofusetariff.hassfurt;version=snapshot,\ io.openems.edge.timeofusetariff.rabotcharge;version=snapshot,\ + io.openems.edge.timeofusetariff.swisspower;version=snapshot,\ io.openems.edge.timeofusetariff.tibber;version=snapshot,\ io.openems.oem.openems;version=snapshot,\ io.openems.shared.influxdb;version=snapshot,\ diff --git a/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImplTest.java b/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImplTest.java index de0aa103841..b0029262635 100644 --- a/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImplTest.java +++ b/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImplTest.java @@ -636,4 +636,26 @@ public void testBatteryProtectionSocLimitations() throws Exception { .output(BP_CHARGE_MAX_SOC, round(40 * 0.2F)) // ); } + + @Test + public void testReadModbus() throws Exception { + var sut = new BatteryFeneconHomeImpl(); + new ComponentTest(sut) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("setModbus", new DummyModbusBridge("modbus0") // + .withRegister(18000, (byte) 0x00, (byte) 0x00)) // TOWER_4_BMS_SOFTWARE_VERSION + .activate(MyConfig.create() // + .setId("battery0") // + .setModbusId("modbus0") // + .setModbusUnitId(0) // + .setStartStop(StartStopConfig.START) // + .setBatteryStartUpRelay("io0/InputOutput4")// + .build()) // + + .next(new TestCase() // + .output(BatteryFeneconHome.ChannelId.TOWER_4_BMS_SOFTWARE_VERSION, 0)) // + + .deactivate(); + } } diff --git a/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/UrlBuilder.java b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/UrlBuilder.java new file mode 100644 index 00000000000..095b1fe2c24 --- /dev/null +++ b/io.openems.edge.bridge.http/src/io/openems/edge/bridge/http/api/UrlBuilder.java @@ -0,0 +1,246 @@ +package io.openems.edge.bridge.http.api; + +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toUnmodifiableMap; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +/** + * Simple URL Builder class to build URLs with correctly encoded query + * parameter. This class is immutable. + * + *

+ * Example Usage: + * + *

+ * 
+ * final var url = UrlBuilder.create() //
+		.withScheme("https") //
+		.withHost("openems.io") //
+		.withPort(443) //
+		.withPath("/path/to") //
+		.withQueryParam("key", "value") //
+		.withFragment("fragment") //
+		// get final result
+		.toUri() // to get a URI Object
+		.toUrl() // to get a URL Object
+		.toEncodedString() // to get the URL as a encoded String
+ * 
+ * or parse from a string
+ * 
+ * final var url = UrlBuilder.from("https://openems.io:443/path?key=value#fragment");
+ * 
+ * 
+ * + *

+ * URL-Schema: + * scheme://host:port/path?queryParams#fragment + * + */ +public final class UrlBuilder { + + /** + * Parses a raw uri string to the url parts. + * + * @param uriString the raw string to parse + * @return the {@link UrlBuilder} with the parts of th uri + */ + public static UrlBuilder parse(String uriString) { + final var uri = URI.create(uriString); + + final var query = uri.getQuery(); + final var queryParams = query == null ? Collections.emptyMap() + : Stream.of(uri.getQuery().split("&")) // + .map(t -> t.split("=")) // + .collect(toUnmodifiableMap(t -> t[0], t -> t[1])); + return new UrlBuilder(// + uri.getScheme(), // + uri.getHost(), // + uri.getPort(), // + uri.getPath(), // + queryParams, // + uri.getFragment() // + ); + } + + /** + * Creates a new {@link UrlBuilder}. + * + * @return the new {@link UrlBuilder} instance + */ + public static UrlBuilder create() { + return new UrlBuilder(null, null, null, null, Collections.emptyMap(), null); + } + + private final String scheme; + private final String host; + private final Integer port; + private final String path; + private final Map queryParams; + private final String fragment; + + private UrlBuilder(// + String scheme, // + String host, // + Integer port, // + String path, // + Map queryParams, // + String fragment // + ) { + this.scheme = scheme; + this.host = host; + this.port = port; + this.path = path; + this.queryParams = queryParams; + this.fragment = fragment; + } + + /** + * Creates a copy of the current {@link UrlBuilder} with the new scheme. + * + * @param scheme the new scheme + * @return the copy of the {@link UrlBuilder} with the new scheme + */ + public UrlBuilder withScheme(String scheme) { + return new UrlBuilder(scheme, this.host, this.port, this.path, this.queryParams, this.fragment); + } + + /** + * Creates a copy of the current {@link UrlBuilder} with the new host. + * + * @param host the new host + * @return the copy of the {@link UrlBuilder} with the new host + */ + public UrlBuilder withHost(String host) { + return new UrlBuilder(this.scheme, host, this.port, this.path, this.queryParams, this.fragment); + } + + /** + * Creates a copy of the current {@link UrlBuilder} with the new port. + * + * @param port the new port + * @return the copy of the {@link UrlBuilder} with the new port + */ + public UrlBuilder withPort(int port) { + if (port < 0) { + throw new IllegalArgumentException("Property 'port' must not be smaller than '0'."); + } + return new UrlBuilder(this.scheme, this.host, port, this.path, this.queryParams, this.fragment); + } + + /** + * Creates a copy of the current {@link UrlBuilder} with the new path. + * + * @param path the new path + * @return the copy of the {@link UrlBuilder} with the new path + */ + public UrlBuilder withPath(String path) { + return new UrlBuilder(this.scheme, this.host, this.port, path, this.queryParams, this.fragment); + } + + /** + * Creates a copy of the current {@link UrlBuilder} with the new query parameter + * added. + * + * @param key the key of the new query parameter + * @param value the value of the new query parameter + * @return the copy of the {@link UrlBuilder} with the new query parameter added + */ + public UrlBuilder withQueryParam(String key, String value) { + Map newQueryParams = new HashMap<>(this.queryParams); + newQueryParams.put(key, value); + return new UrlBuilder(this.scheme, this.host, this.port, this.path, Collections.unmodifiableMap(newQueryParams), + this.fragment); + } + + /** + * Creates a copy of the current {@link UrlBuilder} with the new fragment. + * + * @param fragment the new fragment + * @return the copy of the {@link UrlBuilder} with the new fragment + */ + public UrlBuilder withFragment(String fragment) { + return new UrlBuilder(this.scheme, this.host, this.port, this.path, this.queryParams, fragment); + } + + /** + * Creates a {@link URI} from this object. + * + * @return the {@link URI} + */ + public URI toUri() { + return URI.create(this.toEncodedString()); + } + + /** + * Creates a {@link URI} from this object. + * + * @return the {@link URI} + * @throws MalformedURLException If a protocol handler for the URL could not be + * found, or if some other error occurred while + * constructing the URL + */ + public URL toUrl() throws MalformedURLException { + return this.toUri().toURL(); + } + + /** + * Creates an encoded string url from this object. + * + *

+ * Note: does not check if the url is valid. To Check if it is valid use + * {@link #toUrl()} + * + * @return the encoded url + */ + public String toEncodedString() { + final var url = new StringBuilder(); + + url.append(this.scheme); + url.append("://"); + url.append(this.host); + + if (this.port != null) { + url.append(":"); + url.append(this.port); + } + + if (this.path != null && !this.path.isEmpty()) { + if (!this.path.startsWith("/")) { + url.append("/"); + } + url.append(this.path); + } + + if (!this.queryParams.isEmpty()) { + var query = this.queryParams.entrySet().stream() // + .map(t -> encode(t.getKey()) + "=" + encode(t.getValue())) // + .collect(joining("&", "?", "")); + url.append(query); + } + + if (this.fragment != null && !this.fragment.isEmpty()) { + if (!this.fragment.startsWith("#")) { + url.append("#"); + } + url.append(this.fragment); + } + + return url.toString(); + } + + // Helper method to URL-encode values + private static String encode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8) // + .replace("+", "%20") // " " => "+" => "%20" + ; + } +} diff --git a/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/UrlBuilderTest.java b/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/UrlBuilderTest.java new file mode 100644 index 00000000000..7bfbc193476 --- /dev/null +++ b/io.openems.edge.bridge.http/test/io/openems/edge/bridge/http/api/UrlBuilderTest.java @@ -0,0 +1,117 @@ +package io.openems.edge.bridge.http.api; + +import static org.junit.Assert.assertEquals; + +import java.net.URI; + +import org.junit.Test; + +public class UrlBuilderTest { + + @Test + public void testParse() { + final var rawUrl = "https://openems.io:443/path?key=value#fragment"; + final var parsedUrl = UrlBuilder.parse(rawUrl); + assertEquals(rawUrl, parsedUrl.toEncodedString()); + } + + @Test + public void testParseNoQueryParams() { + final var rawUrl = "https://openems.io:443/path#fragment"; + final var parsedUrl = UrlBuilder.parse(rawUrl); + assertEquals(rawUrl, parsedUrl.toEncodedString()); + } + + @Test + public void testScheme() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io"); + + assertEquals("https://openems.io", url.toEncodedString()); + assertEquals("http://openems.io", url.withScheme("http").toEncodedString()); + } + + @Test + public void testHost() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io"); + + assertEquals("https://openems.io", url.toEncodedString()); + assertEquals("https://better.openems.io", url.withHost("better.openems.io").toEncodedString()); + } + + @Test + public void testPort() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io") // + .withPort(443); + + assertEquals("https://openems.io:443", url.toEncodedString()); + assertEquals("https://openems.io:445", url.withPort(445).toEncodedString()); + } + + @Test + public void testPath() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io") // + .withPath("/path"); + + assertEquals("https://openems.io/path", url.toEncodedString()); + assertEquals("https://openems.io/path/abc", url.withPath("/path/abc").toEncodedString()); + assertEquals("https://openems.io/withoutslash", url.withPath("withoutslash").toEncodedString()); + } + + @Test + public void testQueryParameter() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io") // + .withQueryParam("key", "value"); + + assertEquals("https://openems.io?key=value", url.toEncodedString()); + assertEquals("https://openems.io?key=otherValue", url.withQueryParam("key", "otherValue").toEncodedString()); + } + + @Test + public void testFragment() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io") // + .withFragment("myFragment"); + + assertEquals("https://openems.io#myFragment", url.toEncodedString()); + assertEquals("https://openems.io#myOtherFragment", url.withFragment("myOtherFragment").toEncodedString()); + assertEquals("https://openems.io#with", url.withFragment("#with").toEncodedString()); + } + + @Test + public void testToUri() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io") // + .withPort(443) // + .withPath("/path") // + .withQueryParam("key", "value") // + .withFragment("fragment"); + + assertEquals(URI.create("https://openems.io:443/path?key=value#fragment"), url.toUri()); + } + + @Test + public void testToEncodedString() { + final var url = UrlBuilder.create() // + .withScheme("https") // + .withHost("openems.io") // + .withPort(443) // + .withPath("/path") // + .withQueryParam("key", "va lu+e") // + .withFragment("fragment"); + + assertEquals("https://openems.io:443/path?key=va%20lu%2Be#fragment", url.toEncodedString()); + } + +} diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/test/DummyModbusBridge.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/test/DummyModbusBridge.java index 017e7e7bd6f..e41bae3848c 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/test/DummyModbusBridge.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/test/DummyModbusBridge.java @@ -1,11 +1,22 @@ package io.openems.edge.bridge.modbus.test; +import static io.openems.common.utils.ReflectionUtils.getValueViaReflection; +import static io.openems.edge.common.event.EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE; + import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.HashMap; -import java.util.Map; +import java.util.function.Consumer; + +import org.osgi.service.event.Event; +import com.ghgande.j2mod.modbus.ModbusException; import com.ghgande.j2mod.modbus.io.ModbusTransaction; +import com.ghgande.j2mod.modbus.msg.ModbusRequest; +import com.ghgande.j2mod.modbus.msg.ModbusResponse; +import com.ghgande.j2mod.modbus.net.AbstractModbusListener; +import com.ghgande.j2mod.modbus.procimg.ProcessImage; +import com.ghgande.j2mod.modbus.procimg.SimpleProcessImage; +import com.ghgande.j2mod.modbus.procimg.SimpleRegister; import io.openems.common.exceptions.OpenemsException; import io.openems.edge.bridge.modbus.api.AbstractModbusBridge; @@ -13,14 +24,37 @@ import io.openems.edge.bridge.modbus.api.BridgeModbusTcp; import io.openems.edge.bridge.modbus.api.Config; import io.openems.edge.bridge.modbus.api.LogVerbosity; -import io.openems.edge.bridge.modbus.api.ModbusProtocol; -import io.openems.edge.common.channel.Channel; +import io.openems.edge.bridge.modbus.api.worker.internal.DefectiveComponents; +import io.openems.edge.bridge.modbus.api.worker.internal.TasksSupplier; import io.openems.edge.common.component.OpenemsComponent; public class DummyModbusBridge extends AbstractModbusBridge implements BridgeModbusTcp, BridgeModbus, OpenemsComponent { - private final Map protocols = new HashMap<>(); + private final ModbusTransaction modbusTransaction = new ModbusTransaction() { + @Override + public void execute() throws ModbusException { + this.response = DummyModbusBridge.this.executeModbusRequest(this.request); + } + }; + private final AbstractModbusListener modbusListener = new AbstractModbusListener() { + @Override + public ProcessImage getProcessImage(int unitId) { + return DummyModbusBridge.this.processImage; + } + @Override + public void run() { + } + + @Override + public void stop() { + } + + }; + private final TasksSupplier tasksSupplier; + private final DefectiveComponents defectiveComponents; + + private SimpleProcessImage processImage = null; private InetAddress ipAddress = null; public DummyModbusBridge(String id) { @@ -33,10 +67,20 @@ public DummyModbusBridge(String id, LogVerbosity logVerbosity) { BridgeModbus.ChannelId.values(), // BridgeModbusTcp.ChannelId.values() // ); - for (Channel channel : this.channels()) { + for (var channel : this.channels()) { channel.nextProcessImage(); } - super.activate(null, new Config(id, "", true, logVerbosity, 2)); + super.activate(null, new Config(id, "", false, logVerbosity, 2)); + this.tasksSupplier = getValueViaReflection(this.worker, "tasksSupplier"); + this.defectiveComponents = getValueViaReflection(this.worker, "defectiveComponents"); + } + + private synchronized DummyModbusBridge withProcessImage(Consumer callback) { + if (this.processImage == null) { + this.processImage = new SimpleProcessImage(); + } + callback.accept(this.processImage); + return this; } /** @@ -51,14 +95,82 @@ public DummyModbusBridge withIpAddress(String ipAddress) throws UnknownHostExcep return this; } - @Override - public void addProtocol(String sourceId, ModbusProtocol protocol) { - this.protocols.put(sourceId, protocol); + /** + * Sets the value of a Register. + * + * @param address the Register address + * @param b1 first byte + * @param b2 second byte + * @return myself + */ + public DummyModbusBridge withRegister(int address, byte b1, byte b2) { + return this.withProcessImage(pi -> pi.addRegister(address, new SimpleRegister(b1, b2))); + } + + /** + * Sets the value of a Register. + * + * @param address the Register address + * @param value the value + * @return myself + */ + public DummyModbusBridge withRegister(int address, int value) { + return this.withProcessImage(pi -> pi.addRegister(address, new SimpleRegister(value))); } + /** + * Sets the values of Registers. + * + * @param startAddress the start Register address + * @param values the values + * @return myself + */ + public DummyModbusBridge withRegisters(int startAddress, int... values) { + for (var value : values) { + this.withRegister(startAddress++, value); + } + return this; + } + + /** + * Sets the values of Registers. + * + * @param startAddress the start Register address + * @param values the values + * @return myself + */ + public DummyModbusBridge withRegisters(int startAddress, int[]... values) { + for (var a : values) { + for (var b : a) { + this.withRegister(startAddress++, b); + } + } + return this; + } + + /** + * NOTE: {@link DummyModbusBridge} does not call parent handleEvent(). + */ @Override - public void removeProtocol(String sourceId) { - this.protocols.remove(sourceId); + public synchronized void handleEvent(Event event) { + // NOTE: TOPIC_CYCLE_EXECUTE_WRITE is not implemented (yet) + if (this.processImage == null) { + return; + } + switch (event.getTopic()) { + case TOPIC_CYCLE_BEFORE_PROCESS_IMAGE -> this.onBeforeProcessImage(); + } + } + + private ModbusResponse executeModbusRequest(ModbusRequest request) { + return request.createResponse(this.modbusListener); + } + + private void onBeforeProcessImage() { + var cycleTasks = this.tasksSupplier.getCycleTasks(this.defectiveComponents); + for (var readTask : cycleTasks.reads()) { + readTask.execute(this); + } } @Override @@ -71,12 +183,11 @@ public InetAddress getIpAddress() { @Override public ModbusTransaction getNewModbusTransaction() throws OpenemsException { - throw new UnsupportedOperationException("getNewModbusTransaction() Unsupported by Dummy Class"); + return this.modbusTransaction; } @Override public void closeModbusConnection() { - throw new UnsupportedOperationException("closeModbusConnection() Unsupported by Dummy Class"); } } diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/element/BitsWordElementTest.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/element/BitsWordElementTest.java index 65b0e885a56..d3f068826da 100644 --- a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/element/BitsWordElementTest.java +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/element/BitsWordElementTest.java @@ -3,6 +3,7 @@ import static io.openems.common.channel.AccessMode.READ_WRITE; import static io.openems.common.types.OpenemsType.BOOLEAN; import static io.openems.common.types.OpenemsType.INTEGER; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -129,12 +130,7 @@ public void testNotBoolean() throws Exception { private static ModbusTest.FC3ReadRegisters generateSut() throws IllegalArgumentException, IllegalAccessException, OpenemsException, NoSuchFieldException, SecurityException { var sut = new ModbusTest.FC3ReadRegisters<>(new BitsWordElement(0, null), INTEGER); - - // Some Reflection to properly initialize the BitsWordElement - var field = BitsWordElement.class.getDeclaredField("component"); - field.setAccessible(true); - field.set(sut.element, sut); - + setAttributeViaReflection(sut.element, "component", sut); return sut; } diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponentTest.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponentTest.java index 2ce0c196ec0..2e877e2cb42 100644 --- a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponentTest.java +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponentTest.java @@ -1,12 +1,12 @@ package io.openems.edge.bridge.modbus.sunspec; import static io.openems.edge.bridge.modbus.sunspec.AbstractOpenemsSunSpecComponent.preprocessModbusElements; +import static java.util.stream.IntStream.range; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import java.util.ArrayList; -import java.util.stream.IntStream; import org.junit.Before; import org.junit.Ignore; @@ -24,9 +24,16 @@ import io.openems.edge.bridge.modbus.api.LogVerbosity; import io.openems.edge.bridge.modbus.api.element.ModbusElement; import io.openems.edge.bridge.modbus.api.element.StringWordElement; +import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S1; +import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S101; +import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S103; +import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S701; +import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S701_ACType; import io.openems.edge.bridge.modbus.sunspec.Point.ModbusElementPoint; import io.openems.edge.bridge.modbus.sunspec.dummy.MyConfig; import io.openems.edge.bridge.modbus.sunspec.dummy.MySunSpecComponentImpl; +import io.openems.edge.bridge.modbus.test.DummyModbusBridge; +import io.openems.edge.common.channel.ChannelId; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; @@ -60,6 +67,76 @@ public void changeLogLevel() { java.lang.System.setProperty("org.ops4j.pax.logging.DefaultServiceLog.level", "INFO"); } + @Test + public void testReadFromModbus() throws Exception { + var sut = new MySunSpecComponentImpl(); + new ComponentTest(sut) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("setModbus", new DummyModbusBridge("modbus0") // + .withRegisters(40000, 0x5375, 0x6e53) // isSunSpec + .withRegisters(40002, 1, 66) // Block 1 + .withRegisters(40004, // + new int[] { 0x4D79, 0x204D, 0x616E, 0x7566, 0x6163, 0x7475, 0x7265, 0x7200, 0, 0, 0, 0, + 0, 0, 0, 0 }, // S1_MN + new int[] { 0x4D79, 0x204D, 0x6F64, 0x656C, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // S1_MD + range(0, 43).map(i -> 0).toArray()) // + .withRegisters(40070, 101, 50) // Block 101 + .withRegisters(40072, // + new int[] { 123, 234, 345, 456, 1 }, // + range(0, 45).map(i -> 0).toArray()) // + .withRegisters(40122, 103, 50) // Block 103 + .withRegisters(40124, // + new int[] { 124, 235, 346, 457, 1 }, // + range(0, 45).map(i -> 0).toArray()) // + .withRegisters(40174, 701, 121) // Block 701 + .withRegisters(40176, // + new int[] { 1, }, // + range(0, 120).map(i -> 0).toArray()) // + .withRegisters(40297, 702, 50) // Block 702 + .withRegisters(40299, // + new int[] { 1, }, // + range(0, 49).map(i -> 0).toArray()) // + .withRegisters(40375, 0xFFFF) // END_OF_MAP + ) // + .activate(MyConfig.create() // + .setId("cmp0") // + .setModbusId("modbus0") // + .setModbusUnitId(UNIT_ID) // + .setReadFromModbusBlock(1) // + .build()) + + .next(new TestCase()) // + .next(new TestCase() // + .output(c(S1.MN), null) // + .output(c(S1.MD), null)) + + .next(new TestCase() // + .output(c(S1.MN), "My Manufacturer") // + .output(c(S1.MD), "My Model")) + + .next(new TestCase() // + .output(c(S101.A), null) // + .output(c(S101.APH_A), null) // + .output(c(S103.A), null) // + .output(c(S103.APH_A), null)) // + + .next(new TestCase() // + .output(c(S101.A), 1230F) // + .output(c(S101.APH_A), 2340F) // + .output(c(S701.A_C_TYPE), S701_ACType.UNDEFINED)) // + + .next(new TestCase() // + .output(c(S103.A), 1240F) // + .output(c(S103.APH_A), 2350F) // + .output(c(S701.A_C_TYPE), S701_ACType.SPLIT_PHASE)) // + + .deactivate(); + } + + private static ChannelId c(SunSpecPoint point) { + return point.getChannelId(); + } + private static ImmutableSortedMap.Builder generateSunSpec() { var b = ImmutableSortedMap.naturalOrder() // .put(40000, 0x5375) // SunSpec identifier @@ -67,18 +144,18 @@ private static ImmutableSortedMap.Builder generateSunSpec() { .put(40002, 1) // SunSpec Block-ID .put(40003, 66); // Length of the SunSpec Block - IntStream.range(40004, 40070).forEach(i -> b.put(i, 0)); + range(40004, 40070).forEach(i -> b.put(i, 0)); b // .put(40070, 103) // SunSpec Block-ID .put(40071, 24); // Length of the SunSpec Block - IntStream.range(40072, 40096).forEach(i -> b.put(i, 0)); + range(40072, 40096).forEach(i -> b.put(i, 0)); b // .put(40096, 999) // SunSpec Block-ID .put(40097, 10) // Length of the SunSpec Block .put(40108, 702) // SunSpec Block-ID .put(40109, 50); // Length of the SunSpec Block - IntStream.range(40110, 40160).forEach(i -> b.put(i, 0)); + range(40110, 40160).forEach(i -> b.put(i, 0)); return b; } diff --git a/io.openems.edge.common/src/io/openems/edge/common/host/Host.java b/io.openems.edge.common/src/io/openems/edge/common/host/Host.java index 27366c7d401..85a4b328ec2 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/host/Host.java +++ b/io.openems.edge.common/src/io/openems/edge/common/host/Host.java @@ -17,6 +17,15 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { DISK_IS_FULL(Doc.of(Level.INFO) // .text("Disk is full")), // HOSTNAME(Doc.of(OpenemsType.STRING)), // + + /** + * Operating System Version. + * + *

+ * e. g. 'Raspbian GNU/Linux 11 (bullseye)' or 'Windows 11' + */ + OS_VERSION(Doc.of(OpenemsType.STRING) // + .text("Operating system version")), // ; private final Doc doc; @@ -69,7 +78,7 @@ public default StringReadChannel getHostnameChannel() { } /** - * Gets the Disk is Full Warning State. See {@link ChannelId#HOSTNAME}. + * Gets the hostname. See {@link ChannelId#HOSTNAME}. * * @return the Channel {@link Value} */ @@ -86,4 +95,32 @@ public default void _setHostname(String value) { this.getHostnameChannel().setNextValue(value); } + /** + * Gets the Channel for {@link ChannelId#OS_VERSION}. + * + * @return the Channel + */ + public default StringReadChannel getOsVersionChannel() { + return this.channel(ChannelId.OS_VERSION); + } + + /** + * Gets the operating system version. See {@link ChannelId#OS_VERSION}. + * + * @return the Channel {@link Value} + */ + public default Value getOsVersion() { + return this.getOsVersionChannel().value(); + } + + /** + * Internal method to set the 'nextValue' on {@link ChannelId#OS_VERSION} + * Channel. + * + * @param value the next value + */ + public default void _setOsVersion(String value) { + this.getOsVersionChannel().setNextValue(value); + } + } diff --git a/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java b/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java index e09c9fc0b4d..3f949bce31e 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java +++ b/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java @@ -1,5 +1,9 @@ package io.openems.edge.common.test; +import static io.openems.common.utils.ReflectionUtils.invokeMethodViaReflection; +import static io.openems.common.utils.ReflectionUtils.invokeMethodWithoutArgumentsViaReflection; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; + import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -34,6 +38,7 @@ import io.openems.common.types.ChannelAddress; import io.openems.common.types.OpenemsType; import io.openems.common.types.OptionsEnum; +import io.openems.common.utils.ReflectionUtils.ReflectionException; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.channel.ChannelId; import io.openems.edge.common.channel.EnumDoc; @@ -45,6 +50,7 @@ import io.openems.edge.common.sum.Sum; import io.openems.edge.common.test.AbstractComponentTest.ChannelValue.ChannelAddressValue; import io.openems.edge.common.test.AbstractComponentTest.ChannelValue.ChannelIdValue; +import io.openems.edge.common.test.AbstractComponentTest.ChannelValue.ChannelNameValue; import io.openems.edge.common.test.AbstractComponentTest.ChannelValue.ComponentChannelIdValue; import io.openems.edge.common.type.TypeUtils; @@ -83,6 +89,13 @@ public String toString() { } } + public record ChannelNameValue(String channelName, Object value, boolean force) implements ChannelValue { + @Override + public String toString() { + return this.channelName + ":" + this.value; + } + } + public record ComponentChannelIdValue(String componentId, ChannelId channelId, Object value, boolean force) implements ChannelValue { @Override @@ -196,6 +209,18 @@ public TestCase input(ChannelId channelId, Object value) { return this; } + /** + * Adds an input value for a ChannelId of the system-under-test. + * + * @param channelName the Channel + * @param value the value {@link Object} + * @return myself + */ + public TestCase input(String channelName, Object value) { + this.inputs.add(new ChannelNameValue(channelName, value, false)); + return this; + } + /** * Enforces an input value for a {@link ChannelAddress}. * @@ -257,6 +282,22 @@ public TestCase inputForce(ChannelId channelId, Object value) { return this; } + /** + * Enforces an input value for a Channel of the system-under-test. + * + *

+ * Use this method if you want to be sure, that the Channel actually applies the + * value, e.g. to override a {@link Debounce} setting. + * + * @param channelName the Channel + * @param value the value {@link Object} + * @return myself + */ + public TestCase inputForce(String channelName, Object value) { + this.inputs.add(new ChannelNameValue(channelName, value, true)); + return this; + } + /** * Adds an expected output value for a {@link ChannelAddress}. * @@ -307,6 +348,18 @@ public TestCase output(ChannelId channelId, Object value) { return this; } + /** + * Adds an expected output value for a Channel of the system-under-test. + * + * @param channelName the Channel + * @param value the value {@link Object} + * @return myself + */ + public TestCase output(String channelName, Object value) { + this.outputs.add(new ChannelNameValue(channelName, value, false)); + return this; + } + /** * Adds a simulated timeleap, i.e. simulates that a given amount of time passed. * @@ -522,6 +575,10 @@ private Channel getChannel(AbstractComponentTest act, ChannelValue cv) return act.sut.channel(civ.channelId); } + if (cv instanceof ChannelNameValue civ2) { + return act.sut.channel(civ2.channelName); + } + if (cv instanceof ComponentChannelIdValue cciv) { var component = this.getComponent(act.components, cciv.componentId()); return component.channel(cciv.channelId()); @@ -651,11 +708,9 @@ public SELF addReference(String memberName, Object object) throws Exception { private boolean addReference(Class clazz, String memberName, Object object) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { try { - var field = clazz.getDeclaredField(memberName); - field.setAccessible(true); - field.set(this.sut, object); + setAttributeViaReflection(this.sut, memberName, object); return true; - } catch (NoSuchFieldException e) { + } catch (ReflectionException e) { // Ignore. Try method. if (this.invokeSingleArgMethod(clazz, memberName, object)) { return true; @@ -785,8 +840,7 @@ private boolean callActivateOrModified(String methodName, AbstractComponentConfi } args[i] = arg; } - method.setAccessible(true); - method.invoke(this.sut, args); + invokeMethodViaReflection(this.sut, method, args); return true; } return false; @@ -806,14 +860,10 @@ private void callModified(AbstractComponentConfig config) throws Exception { private void callDeactivate() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { - Class clazz = this.sut.getClass(); - var method = clazz.getDeclaredMethod("deactivate"); - method.setAccessible(true); - method.invoke(this.sut); + invokeMethodWithoutArgumentsViaReflection(this.sut, "deactivate"); } - private boolean invokeSingleArgMethod(Class clazz, String methodName, Object arg) - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + private boolean invokeSingleArgMethod(Class clazz, String methodName, Object arg) throws ReflectionException { var methods = clazz.getDeclaredMethods(); for (Method method : methods) { if (!method.getName().equals(methodName)) { @@ -828,8 +878,7 @@ private boolean invokeSingleArgMethod(Class clazz, String methodName, Object continue; } - method.setAccessible(true); - method.invoke(this.sut, arg); + invokeMethodViaReflection(this.sut, method, arg); return true; } @@ -907,9 +956,11 @@ private static void executeCallbacks(List> callbacks * */ protected void handleEvent(String topic) throws Exception { - if (this.sut instanceof EventHandler) { - var event = new Event(topic, new HashMap()); - ((EventHandler) this.sut).handleEvent(event); + var event = new Event(topic, new HashMap()); + for (var component : this.components.values()) { + if (component instanceof EventHandler eh) { + eh.handleEvent(event); + } } } diff --git a/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyBackendOnRequestFactory.java b/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyBackendOnRequestFactory.java index 05aed052db3..bb10ff7fd07 100644 --- a/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyBackendOnRequestFactory.java +++ b/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyBackendOnRequestFactory.java @@ -1,21 +1,20 @@ package io.openems.edge.controller.api.backend; -import java.lang.reflect.InvocationTargetException; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentServiceObjects; -import io.openems.common.utils.ReflectionUtils; +import io.openems.common.utils.ReflectionUtils.ReflectionException; import io.openems.edge.controller.api.backend.handler.BindingRoutesJsonApiHandler; import io.openems.edge.controller.api.backend.handler.RootRequestHandler; import io.openems.edge.controller.api.common.handler.RoutesJsonApiHandler; public class DummyBackendOnRequestFactory extends BackendOnRequest.Factory { - public DummyBackendOnRequestFactory() - throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + public DummyBackendOnRequestFactory() throws ReflectionException { super(); - ReflectionUtils.setAttribute(BackendOnRequest.Factory.class, this, "cso", new DummyBackendOnRequestCso()); + setAttributeViaReflection(this, "cso", new DummyBackendOnRequestCso()); } private static class DummyBackendOnRequestCso implements ComponentServiceObjects { diff --git a/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyResendHistoricDataWorkerFactory.java b/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyResendHistoricDataWorkerFactory.java index 91aff08460e..55e5e7f439c 100644 --- a/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyResendHistoricDataWorkerFactory.java +++ b/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/DummyResendHistoricDataWorkerFactory.java @@ -1,19 +1,17 @@ package io.openems.edge.controller.api.backend; -import java.lang.reflect.InvocationTargetException; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentServiceObjects; -import io.openems.common.utils.ReflectionUtils; +import io.openems.common.utils.ReflectionUtils.ReflectionException; public class DummyResendHistoricDataWorkerFactory extends ResendHistoricDataWorkerFactory { - public DummyResendHistoricDataWorkerFactory() - throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + public DummyResendHistoricDataWorkerFactory() throws ReflectionException { super(); - ReflectionUtils.setAttribute(ResendHistoricDataWorkerFactory.class, this, "cso", - new DummyResendHistoricDataWorkerCso()); + setAttributeViaReflection(this, "cso", new DummyResendHistoricDataWorkerCso()); } private static class DummyResendHistoricDataWorkerCso implements ComponentServiceObjects { diff --git a/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/DummyJsonRpcRestHandlerFactory.java b/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/DummyJsonRpcRestHandlerFactory.java index 2b99bdb06c3..927c148ca6c 100644 --- a/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/DummyJsonRpcRestHandlerFactory.java +++ b/io.openems.edge.controller.api.rest/test/io/openems/edge/controller/api/rest/DummyJsonRpcRestHandlerFactory.java @@ -1,21 +1,19 @@ package io.openems.edge.controller.api.rest; -import java.lang.reflect.InvocationTargetException; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentServiceObjects; import com.google.common.base.Supplier; -import io.openems.common.utils.ReflectionUtils; +import io.openems.common.utils.ReflectionUtils.ReflectionException; public class DummyJsonRpcRestHandlerFactory extends JsonRpcRestHandler.Factory { - public DummyJsonRpcRestHandlerFactory(Supplier factoryMethod) - throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + public DummyJsonRpcRestHandlerFactory(Supplier factoryMethod) throws ReflectionException { super(); - ReflectionUtils.setAttribute(JsonRpcRestHandler.Factory.class, this, "cso", - new DummyJsonRpcRestHandlerCso(factoryMethod)); + setAttributeViaReflection(this, "cso", new DummyJsonRpcRestHandlerCso(factoryMethod)); } private static class DummyJsonRpcRestHandlerCso implements ComponentServiceObjects { diff --git a/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/DummyOnRequestFactory.java b/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/DummyOnRequestFactory.java index a953b39803c..852eb16321e 100644 --- a/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/DummyOnRequestFactory.java +++ b/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/DummyOnRequestFactory.java @@ -1,17 +1,17 @@ package io.openems.edge.controller.api.websocket; -import java.lang.reflect.InvocationTargetException; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentServiceObjects; -import io.openems.common.utils.ReflectionUtils; +import io.openems.common.utils.ReflectionUtils.ReflectionException; public class DummyOnRequestFactory extends OnRequest.Factory { - public DummyOnRequestFactory() throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + public DummyOnRequestFactory() throws ReflectionException { super(); - ReflectionUtils.setAttribute(OnRequest.Factory.class, this, "cso", new DummyOnRequestCso()); + setAttributeViaReflection(this, "cso", new DummyOnRequestCso()); } private static class DummyOnRequestCso implements ComponentServiceObjects { diff --git a/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/ControllerEvcsImplTest.java b/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/ControllerEvcsImplTest.java index b946f3090e8..e51964f0f59 100644 --- a/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/ControllerEvcsImplTest.java +++ b/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/ControllerEvcsImplTest.java @@ -20,21 +20,16 @@ import org.junit.Test; -import io.openems.edge.common.filter.DisabledRampFilter; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.Status; -import io.openems.edge.evcs.test.DummyEvcsPower; import io.openems.edge.evcs.test.DummyManagedEvcs; public class ControllerEvcsImplTest { - private static final DummyEvcsPower EVCS_POWER = new DummyEvcsPower(new DisabledRampFilter()); - private static final DummyManagedEvcs EVCS = new DummyManagedEvcs("evcs0", EVCS_POWER); - private static final int DEFAULT_FORCE_CHARGE_MIN_POWER = 7360; private static final int DEFAULT_CHARGE_MIN_POWER = 0; @@ -43,7 +38,7 @@ public void excessChargeTest1() throws Exception { new ControllerTest(new ControllerEvcsImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // .setEvcsId("evcs0") // @@ -68,7 +63,7 @@ public void excessChargeTest2() throws Exception { new ControllerTest(new ControllerEvcsImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // .setEvcsId("evcs0") // @@ -95,7 +90,7 @@ public void forceChargeTest() throws Exception { new ControllerTest(new ControllerEvcsImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // .setEvcsId("evcs0") // @@ -120,7 +115,7 @@ public void chargingDisabledTest() throws Exception { new ControllerTest(new ControllerEvcsImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // .setEvcsId("evcs0") // @@ -142,7 +137,7 @@ public void wrongConfigParametersTest() throws Exception { new ControllerTest(new ControllerEvcsImpl()) // .addReference("cm", cm) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // .setEvcsId("evcs0") // @@ -167,7 +162,7 @@ public void clusterTest() throws Exception { new ControllerTest(new ControllerEvcsImpl(clock)) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // .setEvcsId("evcs0") // @@ -215,7 +210,7 @@ public void clusterTestDisabledCharging() throws Exception { new ControllerTest(new ControllerEvcsImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // .setEvcsId("evcs0") // @@ -264,7 +259,7 @@ public void hysteresisTest() throws Exception { new ControllerTest(new ControllerEvcsImpl(clock)) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("sum", new DummySum()) // - .addReference("evcs", EVCS) // + .addReference("evcs", DummyManagedEvcs.ofDisabled("evcs0")) // .activate(MyConfig.create() // .setId("ctrlEvcs0") // .setEvcsId("evcs0") // diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Swisspower.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Swisspower.java new file mode 100644 index 00000000000..8726789cf11 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Swisspower.java @@ -0,0 +1,197 @@ +package io.openems.edge.app.timeofusetariff; + +import static io.openems.edge.core.appmanager.formly.enums.InputType.PASSWORD; +import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; +import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; + +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.common.collect.Lists; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.common.props.CommonProps; +import io.openems.edge.app.timeofusetariff.Swisspower.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentManagerSupplier; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.Nameable; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.dependency.Tasks; +import io.openems.edge.core.appmanager.dependency.aggregatetask.SchedulerByCentralOrderConfiguration.SchedulerComponent; +import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; + +/** + * Describes a App for Swisspower. + * + *

+  {
+    "appId":"App.TimeOfUseTariff.Swisspower",
+    "alias":"Swisspower",
+    "instanceId": UUID,
+    "image": base64,
+    "properties":{
+    	"CTRL_ESS_TIME_OF_USE_TARIFF_ID": "ctrlEssTimeOfUseTariff0",
+    	"TIME_OF_USE_TARIFF_PROVIDER_ID": "timeOfUseTariff0",
+    	"ACCESS_TOKEN": {token},
+    	"METERING_CODE": {code}
+    },
+    "appDescriptor": {
+    	"websiteUrl": {@link AppDescriptor#getWebsiteUrl()}
+    }
+  }
+ * 
+ */ +@Component(name = "App.TimeOfUseTariff.Swisspower") +public class Swisspower extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public static enum Property implements Type, Nameable { + // Component-IDs + CTRL_ESS_TIME_OF_USE_TARIFF_ID(AppDef.componentId("ctrlEssTimeOfUseTariff0")), // + TIME_OF_USE_TARIFF_PROVIDER_ID(AppDef.componentId("timeOfUseTariff0")), // + + // Properties + ALIAS(CommonProps.alias()), // + ACCESS_TOKEN(AppDef.copyOfGeneric(CommonProps.defaultDef(), def -> def// + .setTranslatedLabelWithAppPrefix(".accessToken.label") // + .setTranslatedDescriptionWithAppPrefix(".accessToken.description") // + .setRequired(true) // + .setField(JsonFormlyUtil::buildInput, (app, prop, l, params, field) -> { + field.setInputType(PASSWORD); + }) // + .bidirectional(TIME_OF_USE_TARIFF_PROVIDER_ID, "accessToken", + ComponentManagerSupplier::getComponentManager, t -> { + return JsonUtils.getAsOptionalString(t) // + .map(s -> { + if (s.isEmpty()) { + return null; + } + return new JsonPrimitive("xxx"); + }) // + .orElse(null); + }))), + METERING_CODE(AppDef.copyOfGeneric(CommonProps.defaultDef(), def -> def// + .setTranslatedLabelWithAppPrefix(".meteringCode.label") // + .setTranslatedDescriptionWithAppPrefix(".meteringCode.description") // + .setRequired(true) // + .setField(JsonFormlyUtil::buildInput))); + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Property self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, Type.Parameter.BundleParameter> getParamter() { + return Type.Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public Swisspower(@Reference ComponentManager componentManager, ComponentContext context, + @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + super(componentManager, context, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + final var ctrlEssTimeOfUseTariffId = this.getId(t, p, Property.CTRL_ESS_TIME_OF_USE_TARIFF_ID); + final var timeOfUseTariffProviderId = this.getId(t, p, Property.TIME_OF_USE_TARIFF_PROVIDER_ID); + + final var alias = this.getString(p, l, Property.ALIAS); + final var accessToken = this.getValueOrDefault(p, Property.ACCESS_TOKEN, null); + final var meteringCode = this.getString(p, l, Property.METERING_CODE); + + var components = Lists.newArrayList(// + new EdgeConfig.Component(ctrlEssTimeOfUseTariffId, alias, "Controller.Ess.Time-Of-Use-Tariff", + JsonUtils.buildJsonObject() // + .addProperty("ess.id", "ess0") // + .build()), // + new EdgeConfig.Component(timeOfUseTariffProviderId, this.getName(l), "TimeOfUseTariff.Swisspower", + JsonUtils.buildJsonObject() // + .addPropertyIfNotNull("meteringCode", meteringCode) // + .onlyIf(accessToken != null && !accessToken.equals("xxx"), b -> { + b.addProperty("accessToken", accessToken); + }) // + .build())// + ); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.schedulerByCentralOrder(new SchedulerComponent(ctrlEssTimeOfUseTariffId, + "Controller.Ess.Time-Of-Use-Tariff", this.getAppId()))) // + .addTask(Tasks.persistencePredictor("_sum/UnmanagedConsumptionActivePower")) // + .build(); + }; + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.TIME_OF_USE_TARIFF }; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE_IN_CATEGORY; + } + + @Override + protected ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); + } + + @Override + protected Swisspower getApp() { + return this; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties index 927b28bf399..809b90e995c 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties @@ -451,6 +451,12 @@ App.TimeOfUseTariff.Stromdao.Name = Dynamischer Stromtarif (Stromdao Corrently) App.TimeOfUseTariff.Stromdao.Name.short = Stromdao Corrently App.TimeOfUseTariff.Stromdao.zipCode.label = PLZ App.TimeOfUseTariff.Stromdao.zipCode.description = Deutsche Postleitzahl des Wohnorts +App.TimeOfUseTariff.Swisspower.Name = Dynamischer Stromtarif (Swisspower) +App.TimeOfUseTariff.Swisspower.Name.short = Swisspower +App.TimeOfUseTariff.Swisspower.accessToken.label = Token +App.TimeOfUseTariff.Swisspower.accessToken.description = Bitte stellen Sie den von Swisspower bereitgestellte Zugangstoken bereit. Um ein Token zu erhalten, wenden Sie sich bitte an Swisspower. +App.TimeOfUseTariff.Swisspower.meteringCode.label = Messpunktnummer (z.B. CH1018601234500000000000000011642) +App.TimeOfUseTariff.Swisspower.meteringCode.description = Bitte stellen Sie die von Swisspower bereitgestellte Messpunktnummer bereit. Um ein Messpunktnummer zu erhalten, wenden Sie sich bitte an Swisspower. App.TimeOfUseTariff.Tibber.Name = Dynamischer Stromtarif (Tibber) App.TimeOfUseTariff.Tibber.Name.short = Tibber App.TimeOfUseTariff.Tibber.accessToken.label = Token diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties index 538f2862b93..3614d8cb9f6 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties @@ -451,6 +451,12 @@ App.TimeOfUseTariff.Stromdao.Name = Time-of-Use Tariff (STROMDAO Corrently) App.TimeOfUseTariff.Stromdao.Name.short = STROMDAO Corrently App.TimeOfUseTariff.Stromdao.zipCode.label = ZIP Code App.TimeOfUseTariff.Stromdao.zipCode.description = German postal code of place of residence +App.TimeOfUseTariff.Swisspower.Name = Time-of-Use Tariff (Swisspower) +App.TimeOfUseTariff.Swisspower.Name.short = Swisspower +App.TimeOfUseTariff.Swisspower.accessToken.label = Token +App.TimeOfUseTariff.Swisspower.accessToken.description = Please provide personal access token provided by Swisspower. To get one, please contact Swisspower. +App.TimeOfUseTariff.Swisspower.meteringCode.label = Measuring point number (e.g. CH1018601234500000000000000011642) +App.TimeOfUseTariff.Swisspower.meteringCode.description = Please provide Measuring point number received by Swisspower. To get one, please contact Swisspower. App.TimeOfUseTariff.Tibber.Name = Time-of-Use Tariff (Tibber) App.TimeOfUseTariff.Tibber.Name.short = Tibber App.TimeOfUseTariff.Tibber.accessToken.label = Token diff --git a/io.openems.edge.core/src/io/openems/edge/core/host/HostImpl.java b/io.openems.edge.core/src/io/openems/edge/core/host/HostImpl.java index bc18b13fab4..b1fa2ec2900 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/host/HostImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/host/HostImpl.java @@ -16,6 +16,7 @@ import org.osgi.service.component.annotations.Reference; import org.osgi.service.metatype.annotations.Designate; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; @@ -50,6 +51,8 @@ }) public class HostImpl extends AbstractOpenemsComponent implements Host, OpenemsComponent, ComponentJsonApi { + private final Logger log = LoggerFactory.getLogger(HostImpl.class); + protected final OperatingSystem operatingSystem; private final DiskSpaceWorker diskSpaceWorker; @@ -93,6 +96,13 @@ public HostImpl() { e1.printStackTrace(); } } + + this.operatingSystem.getOperatingSystemVersion().whenComplete((name, error) -> { + this._setOsVersion(name); + if (error != null) { + this.log.info("Error while trying to get operating system version", error); + } + }); } @Activate diff --git a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystem.java b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystem.java index ef8c3cacbb3..5a39d0b9510 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystem.java +++ b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystem.java @@ -61,4 +61,11 @@ public CompletableFuture handleExecuteSystemComman public CompletableFuture handleExecuteSystemRestartRequest( ExecuteSystemRestartRequest request) throws NotImplementedException; + /** + * Gets the current operating system version. + * + * @return a future with the result + */ + public CompletableFuture getOperatingSystemVersion(); + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemDebianSystemd.java b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemDebianSystemd.java index f74bd442b16..49e3e99764c 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemDebianSystemd.java +++ b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemDebianSystemd.java @@ -29,6 +29,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; import java.util.regex.Pattern; +import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -203,7 +204,7 @@ private List toFileFormat(User user, NetworkInterface iface) throws O + user.getName()); result.add("# changedAt: " // + LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES).toString()); - + // Match Section result.add(MATCH_SECTION); result.add("Name=" + iface.getName()); @@ -220,12 +221,12 @@ private List toFileFormat(User user, NetworkInterface iface) throws O if (iface.getLinkLocalAddressing().isSetAndNotNull()) { result.add("LinkLocalAddressing=" + (iface.getLinkLocalAddressing().getValue() ? "yes" : "no")); } - + var metric = DEFAULT_METRIC; if (iface.getMetric().isSetAndNotNull()) { metric = iface.getMetric().getValue().intValue(); } - + if (iface.getDhcp().isSetAndNotNull()) { var dhcp = iface.getDhcp().getValue(); result.add(EMPTY_SECTION); @@ -577,4 +578,35 @@ protected static NetworkInterface parseSystemdNetworkdConfigurationFile(L dhcp.get(), linkLocalAddressing.get(), gateway.get(), dns.get(), addresses.get(), metric.get(), attachment); } + + @Override + public CompletableFuture getOperatingSystemVersion() { + final var sc = new SystemCommand(// + "cat /etc/os-release", // + false, // runInBackground + 5, // timeoutSeconds + Optional.empty(), // username + Optional.empty()); // password + + final var versionFuture = new CompletableFuture(); + this.execute(sc, success -> { + final var osVersionName = Stream.of(success.stdout()) // + .map(t -> t.split("=", 2)) // + .filter(t -> t.length == 2) // + .filter(t -> t[0].equals("PRETTY_NAME")) // + .map(t -> t[1]) // + .map(t -> { + if (t.startsWith("\"") && t.endsWith("\"")) { + return t.substring(1, t.length() - 1); + } + return t; + }) // + .findAny(); + + osVersionName.ifPresentOrElse(versionFuture::complete, () -> versionFuture + .completeExceptionally(new OpenemsException("OS-Version name not found in /etc/os-release"))); + }, versionFuture::completeExceptionally); + return versionFuture; + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemWindows.java b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemWindows.java index 68c509a86a4..f41c76e7ad5 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemWindows.java +++ b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemWindows.java @@ -50,4 +50,9 @@ public CompletableFuture handleExecuteSystemRe throw new NotImplementedException("ExecuteSystemRestartRequest is not implemented for Windows"); } + @Override + public CompletableFuture getOperatingSystemVersion() { + return CompletableFuture.completedFuture(System.getProperty("os.name")); + } + } diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java index 1252dc6b574..2ec9fd378ee 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java @@ -1,5 +1,7 @@ package io.openems.edge.core.appmanager; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; +import static io.openems.common.utils.ReflectionUtils.setStaticAttributeViaReflection; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -33,7 +35,6 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.JsonUtils; -import io.openems.common.utils.ReflectionUtils; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.host.Host; import io.openems.edge.common.test.ComponentTest; @@ -183,8 +184,6 @@ public AppManagerTestBundle(// this.appManagerUtil = new AppManagerUtilImpl(this.componentManger); this.appCenterBackendUtil = new DummyAppCenterBackendUtil(); - ReflectionUtils.setAttribute(this.appManagerUtil.getClass(), this.appManagerUtil, "appManager", this.sut); - this.addCheckable(TestCheckable.COMPONENT_NAME, t -> new TestCheckable()); this.addCheckable(CheckOr.COMPONENT_NAME, t -> new CheckOr(t, this.checkableFactory)); this.checkCardinality = this.addCheckable(CheckCardinality.COMPONENT_NAME, @@ -198,20 +197,13 @@ public AppManagerTestBundle(// this.appValidateWorker = new AppValidateWorker(); final var appConfigValidator = new AppConfigValidator(); - ReflectionUtils.setAttribute(AppValidateWorker.class, this.appValidateWorker, "appManagerUtil", - this.appManagerUtil); - ReflectionUtils.setAttribute(AppValidateWorker.class, this.appValidateWorker, "validator", appConfigValidator); - - ReflectionUtils.setAttribute(AppConfigValidator.class, appConfigValidator, "appManagerUtil", - this.appManagerUtil); - ReflectionUtils.setAttribute(AppConfigValidator.class, appConfigValidator, "tasks", this.appHelper.getTasks()); + setAttributeViaReflection(this.appValidateWorker, "appManagerUtil", this.appManagerUtil); + setAttributeViaReflection(this.appValidateWorker, "validator", appConfigValidator); - // use this so the appManagerAppHelper does not has to be a OpenemsComponent and - // the attribute can still be private - ReflectionUtils.setAttribute(this.appHelper.getClass(), this.appHelper, "appManager", this.sut); - ReflectionUtils.setAttribute(this.appHelper.getClass(), this.appHelper, "appManagerUtil", this.appManagerUtil); + setAttributeViaReflection(appConfigValidator, "appManagerUtil", this.appManagerUtil); + setAttributeViaReflection(appConfigValidator, "tasks", this.appHelper.getTasks()); - ReflectionUtils.setAttribute(DependencyUtil.class, null, "appHelper", this.appHelper); + setStaticAttributeViaReflection(DependencyUtil.class, "appHelper", this.appHelper); new ComponentTest(this.sut) // .addReference("cm", this.cm) // diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java index 4fa625f043d..a5892cd16c8 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java @@ -67,6 +67,7 @@ import io.openems.edge.app.timeofusetariff.RabotCharge; import io.openems.edge.app.timeofusetariff.StadtwerkHassfurt; import io.openems.edge.app.timeofusetariff.StromdaoCorrently; +import io.openems.edge.app.timeofusetariff.Swisspower; import io.openems.edge.app.timeofusetariff.Tibber; import io.openems.edge.common.component.ComponentManager; @@ -175,6 +176,16 @@ public static final StadtwerkHassfurt stadtwerkHassfurt(AppManagerTestBundle t) return app(t, StadtwerkHassfurt::new, "App.TimeOfUseTariff.Hassfurt"); } + /** + * Test method for creating a {@link Swisspower}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final Swisspower swisspower(AppManagerTestBundle t) { + return app(t, Swisspower::new, "App.TimeOfUseTariff.Swisspower"); + } + /** * Test method for creating a {@link RabotCharge}. * diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java index 9fe35111886..9ad1f56bc01 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java @@ -1,11 +1,12 @@ package io.openems.edge.core.appmanager; -import java.lang.reflect.InvocationTargetException; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; + import java.util.ArrayList; import java.util.List; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.utils.ReflectionUtils; +import io.openems.common.utils.ReflectionUtils.ReflectionException; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.user.User; import io.openems.edge.core.appmanager.dependency.AppManagerAppHelper; @@ -24,12 +25,12 @@ public DummyAppManagerAppHelper(// ComponentManager componentManager, // ComponentUtil componentUtil, // AppManagerUtil util // - ) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + ) throws ReflectionException { this.tasks = new ArrayList>(); this.impl = new AppManagerAppHelperImpl(componentManager, componentUtil); - ReflectionUtils.setAttribute(AppManagerAppHelperImpl.class, this.impl, "tasks", this.tasks); - ReflectionUtils.setAttribute(AppManagerAppHelperImpl.class, this.impl, "appManagerUtil", util); + setAttributeViaReflection(this.impl, "tasks", this.tasks); + setAttributeViaReflection(this.impl, "appManagerUtil", util); } /** diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java index c14160b59af..252f68b5d30 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java @@ -46,6 +46,9 @@ public void beforeEach() throws Exception { .build())); this.apps.add(new TestTranslation(Apps.stadtwerkHassfurt(t), true, JsonUtils.buildJsonObject() // .build())); + this.apps.add(new TestTranslation(Apps.swisspower(t), true, JsonUtils.buildJsonObject() // + .addProperty("METERING_CODE", "bf7777") // + .build())); this.apps.add(new TestTranslation(Apps.stromdaoCorrently(t), true, JsonUtils.buildJsonObject() // .addProperty("ZIP_CODE", "123456789") // .build())); diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/EnergySchedulerImplTest.java b/io.openems.edge.energy/test/io/openems/edge/energy/EnergySchedulerImplTest.java index c5598a34647..bcf2d42086a 100644 --- a/io.openems.edge.energy/test/io/openems/edge/energy/EnergySchedulerImplTest.java +++ b/io.openems.edge.energy/test/io/openems/edge/energy/EnergySchedulerImplTest.java @@ -1,6 +1,7 @@ package io.openems.edge.energy; import static io.openems.common.utils.DateUtils.roundDownToQuarter; +import static io.openems.common.utils.ReflectionUtils.getValueViaReflection; import static io.openems.edge.energy.LogVerbosity.TRACE; import static io.openems.edge.energy.api.EnergyConstants.SUM_PRODUCTION; import static io.openems.edge.energy.api.EnergyConstants.SUM_UNMANAGED_CONSUMPTION; @@ -126,9 +127,7 @@ public static EnergySchedulerImpl create(Clock clock) throws Exception { * @throws Exception on error */ public static Optimizer getOptimizer(EnergySchedulerImpl energyScheduler) throws Exception { - var field = EnergySchedulerImpl.class.getDeclaredField("optimizer"); - field.setAccessible(true); - return (Optimizer) field.get(energyScheduler); + return getValueViaReflection(energyScheduler, "optimizer"); } } diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/OptimizerTest.java b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/OptimizerTest.java index 6dc5d0c1c3a..e2c2dd7d3c1 100644 --- a/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/OptimizerTest.java +++ b/io.openems.edge.energy/test/io/openems/edge/energy/optimizer/OptimizerTest.java @@ -1,5 +1,6 @@ package io.openems.edge.energy.optimizer; +import static io.openems.common.utils.ReflectionUtils.getValueViaReflection; import static io.openems.edge.energy.EnergySchedulerImplTest.CLOCK; import static io.openems.edge.energy.EnergySchedulerImplTest.getOptimizer; import static org.junit.Assert.assertEquals; @@ -9,6 +10,7 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.function.ThrowingSupplier; +import io.openems.common.utils.ReflectionUtils.ReflectionException; import io.openems.edge.controller.ess.timeofusetariff.StateMachine; import io.openems.edge.energy.EnergySchedulerImplTest; import io.openems.edge.energy.LogVerbosity; @@ -62,14 +64,11 @@ public void test2() { * * @param optimizer the {@link Optimizer} * @return the object - * @throws Exception on error + * @throws ReflectionException on error */ - @SuppressWarnings("unchecked") public static ThrowingSupplier getGlobalSimulationContextSupplier( - Optimizer optimizer) throws Exception { - var field = Optimizer.class.getDeclaredField("gscSupplier"); - field.setAccessible(true); - return (ThrowingSupplier) field.get(optimizer); + Optimizer optimizer) throws ReflectionException { + return getValueViaReflection(optimizer, "gscSupplier"); } } diff --git a/io.openems.edge.energy/test/io/openems/edge/energy/v1/EnergySchedulerImplTest.java b/io.openems.edge.energy/test/io/openems/edge/energy/v1/EnergySchedulerImplTest.java index b283c7fbfcd..f7ff09d480a 100644 --- a/io.openems.edge.energy/test/io/openems/edge/energy/v1/EnergySchedulerImplTest.java +++ b/io.openems.edge.energy/test/io/openems/edge/energy/v1/EnergySchedulerImplTest.java @@ -1,6 +1,8 @@ package io.openems.edge.energy.v1; import static io.openems.common.utils.DateUtils.roundDownToQuarter; +import static io.openems.common.utils.ReflectionUtils.getValueViaReflection; +import static io.openems.common.utils.ReflectionUtils.invokeMethodWithoutArgumentsViaReflection; import static io.openems.edge.energy.optimizer.TestData.CONSUMPTION_PREDICTION_QUARTERLY; import static io.openems.edge.energy.optimizer.TestData.HOURLY_PRICES_SUMMER; import static io.openems.edge.energy.optimizer.TestData.PRODUCTION_PREDICTION_QUARTERLY; @@ -18,6 +20,8 @@ import org.junit.Test; import io.openems.common.test.TimeLeapClock; +import io.openems.common.utils.ReflectionUtils; +import io.openems.common.utils.ReflectionUtils.ReflectionException; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; @@ -89,12 +93,10 @@ public static EnergySchedulerImpl create(Clock clock) throws Exception { * * @param energyScheduler the {@link EnergySchedulerImpl} * @return the object - * @throws Exception on error + * @throws ReflectionException on error */ - public static OptimizerV1 getOptimizer(EnergySchedulerImpl energyScheduler) throws Exception { - var field = EnergySchedulerImpl.class.getDeclaredField("optimizerV1"); - field.setAccessible(true); - return (OptimizerV1) field.get(energyScheduler); + public static OptimizerV1 getOptimizer(EnergySchedulerImpl energyScheduler) throws ReflectionException { + return getValueViaReflection(energyScheduler, "optimizerV1"); } /** @@ -105,9 +107,7 @@ public static OptimizerV1 getOptimizer(EnergySchedulerImpl energyScheduler) thro * @throws Exception on error */ public static void callCreateParams(OptimizerV1 optimizer) throws Exception { - var method = OptimizerV1.class.getDeclaredMethod("createParams"); - method.setAccessible(true); - method.invoke(optimizer); + invokeMethodWithoutArgumentsViaReflection(optimizer, "createParams"); } /** @@ -117,11 +117,8 @@ public static void callCreateParams(OptimizerV1 optimizer) throws Exception { * @return the object * @throws Exception on error */ - @SuppressWarnings("unchecked") public static GlobalContextV1 getGlobalContext(EnergySchedulerImpl energyScheduler) throws Exception { var optimizer = getOptimizer(energyScheduler); - var field = OptimizerV1.class.getDeclaredField("globalContext"); - field.setAccessible(true); - return ((Supplier) field.get(optimizer)).get(); + return ReflectionUtils.>getValueViaReflection(optimizer, "globalContext").get(); } } diff --git a/io.openems.edge.evcs.api/src/io/openems/edge/evcs/test/DummyManagedEvcs.java b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/test/DummyManagedEvcs.java index 18a2eb563a2..75d55e4b902 100644 --- a/io.openems.edge.evcs.api/src/io/openems/edge/evcs/test/DummyManagedEvcs.java +++ b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/test/DummyManagedEvcs.java @@ -10,6 +10,7 @@ import io.openems.edge.common.channel.Channel; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; +import io.openems.edge.common.filter.DisabledRampFilter; import io.openems.edge.common.test.TestUtils; import io.openems.edge.evcs.api.AbstractManagedEvcsComponent; import io.openems.edge.evcs.api.Evcs; @@ -27,7 +28,21 @@ public class DummyManagedEvcs extends AbstractManagedEvcsComponent private int maximumHardwarePower = Evcs.DEFAULT_MAXIMUM_HARDWARE_POWER; private MeterType meterType = MANAGED_CONSUMPTION_METERED; + /** + * Instantiates a disabled {@link DummyManagedEvcs}. + * + * @param id the Component-ID + * @return a new {@link DummyManagedEvcs} + */ + public static DummyManagedEvcs ofDisabled(String id) { + return new DummyManagedEvcs(id, new DummyEvcsPower(new DisabledRampFilter()), false); + } + public DummyManagedEvcs(String id, EvcsPower evcsPower) { + this(id, evcsPower, true); + } + + private DummyManagedEvcs(String id, EvcsPower evcsPower, boolean isEnabled) { super(// OpenemsComponent.ChannelId.values(), // ElectricityMeter.ChannelId.values(), // @@ -38,7 +53,7 @@ public DummyManagedEvcs(String id, EvcsPower evcsPower) { for (Channel channel : this.channels()) { channel.nextProcessImage(); } - super.activate(null, id, "", true); + super.activate(null, id, "", isEnabled); } /** @@ -65,6 +80,9 @@ public DummyManagedEvcs withActivePower(Integer value) { @Override public void handleEvent(Event event) { + if (!this.isEnabled()) { + return; + } super.handleEvent(event); switch (event.getTopic()) { // Results of the written limits are checked after write in the Dummy Component diff --git a/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/EvcsClusterPeakShavingImplTest.java b/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/EvcsClusterPeakShavingImplTest.java index 42c4a4310e9..ba820f14e0b 100644 --- a/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/EvcsClusterPeakShavingImplTest.java +++ b/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/EvcsClusterPeakShavingImplTest.java @@ -18,8 +18,6 @@ import org.junit.Test; -import io.openems.edge.common.filter.DisabledRampFilter; -import io.openems.edge.common.filter.RampFilter; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; @@ -28,7 +26,6 @@ import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.evcs.api.ChargeState; import io.openems.edge.evcs.api.Status; -import io.openems.edge.evcs.test.DummyEvcsPower; import io.openems.edge.evcs.test.DummyManagedEvcs; import io.openems.edge.meter.test.DummyElectricityMeter; @@ -38,13 +35,12 @@ public class EvcsClusterPeakShavingImplTest { private static final DummyManagedSymmetricEss ESS = new DummyManagedSymmetricEss("ess0") // .withMaxApparentPower(30000); private static final DummyElectricityMeter METER = new DummyElectricityMeter("meter0"); - private static final DummyEvcsPower EVCS_POWER = new DummyEvcsPower(new DisabledRampFilter()); - private static final DummyManagedEvcs EVCS0 = new DummyManagedEvcs("evcs0", EVCS_POWER); - private static final DummyManagedEvcs EVCS1 = new DummyManagedEvcs("evcs1", EVCS_POWER); - private static final DummyManagedEvcs EVCS2 = new DummyManagedEvcs("evcs2", EVCS_POWER); - private static final DummyManagedEvcs EVCS3 = new DummyManagedEvcs("evcs3", EVCS_POWER); - private static final DummyManagedEvcs EVCS4 = new DummyManagedEvcs("evcs4", EVCS_POWER); - private static final DummyManagedEvcs EVCS5 = new DummyManagedEvcs("evcs5", new DummyEvcsPower(new RampFilter())); + private static final DummyManagedEvcs EVCS0 = DummyManagedEvcs.ofDisabled("evcs0"); + private static final DummyManagedEvcs EVCS1 = DummyManagedEvcs.ofDisabled("evcs1"); + private static final DummyManagedEvcs EVCS2 = DummyManagedEvcs.ofDisabled("evcs2"); + private static final DummyManagedEvcs EVCS3 = DummyManagedEvcs.ofDisabled("evcs3"); + private static final DummyManagedEvcs EVCS4 = DummyManagedEvcs.ofDisabled("evcs4"); + private static final DummyManagedEvcs EVCS5 = DummyManagedEvcs.ofDisabled("evcs5"); private static final int HARDWARE_POWER_LIMIT_PER_PHASE = 7000; diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadWorker.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadWorker.java index 14c23d0e9c7..e5c078d0a27 100644 --- a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadWorker.java +++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadWorker.java @@ -80,27 +80,46 @@ protected void forever() throws InterruptedException { @Override protected int getCycleTime() { // get minimum required time till next report - var now = LocalDateTime.now(); - if (this.lastReport1.isBefore(now.minusSeconds(Report.REPORT1.getRequestSeconds())) - || this.lastReport2.isBefore(now.minusSeconds(Report.REPORT2.getRequestSeconds())) - || this.lastReport3.isBefore(now.minusSeconds(Report.REPORT3.getRequestSeconds()))) { + return getCycleTimeLogic(this.lastReport1, this.lastReport2, this.lastReport3, LocalDateTime.now()); + } + + /** + * Calculates the cycletime for given dateTimes. + * + * @param lastReport1 last time report 1 was read + * @param lastReport2 last time report 2 was read + * @param lastReport3 last time report 3 was read + * @param now current time + * @return the time until the next cycle + */ + public static int getCycleTimeLogic(LocalDateTime lastReport1, LocalDateTime lastReport2, LocalDateTime lastReport3, + LocalDateTime now) { + if (lastReport1.isBefore(now.minusSeconds(Report.REPORT1.getRequestSeconds())) + || lastReport2.isBefore(now.minusSeconds(Report.REPORT2.getRequestSeconds())) + || lastReport3.isBefore(now.minusSeconds(Report.REPORT3.getRequestSeconds()))) { return 0; } - var tillReport1 = ChronoUnit.MILLIS.between(now.minusSeconds(Report.REPORT1.getRequestSeconds()), - this.lastReport1); - var tillReport2 = ChronoUnit.MILLIS.between(now.minusSeconds(Report.REPORT2.getRequestSeconds()), - this.lastReport2); - var tillReport3 = ChronoUnit.MILLIS.between(now.minusSeconds(Report.REPORT3.getRequestSeconds()), - this.lastReport3); - var min = Math.min(Math.min(tillReport1, tillReport2), tillReport3); - if (min < 0) { + try { + var tillReport1 = ChronoUnit.MILLIS.between(now.minusSeconds(Report.REPORT1.getRequestSeconds()), + lastReport1); + var tillReport2 = ChronoUnit.MILLIS.between(now.minusSeconds(Report.REPORT2.getRequestSeconds()), + lastReport2); + var tillReport3 = ChronoUnit.MILLIS.between(now.minusSeconds(Report.REPORT3.getRequestSeconds()), + lastReport3); + var min = Math.min(Math.min(tillReport1, tillReport2), tillReport3); + if (min < 0) { + return 0; + } + if (min > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else { + return (int) min; + } + } catch (ArithmeticException e) { + // if difference in tillReportX is too large a longOverflow might be thrown return 0; } - if (min > Integer.MAX_VALUE) { - return Integer.MAX_VALUE; - } else { - return (int) min; - } + } @Override diff --git a/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/ReadWorkerTest.java b/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/ReadWorkerTest.java new file mode 100644 index 00000000000..713c77047dc --- /dev/null +++ b/io.openems.edge.evcs.keba.kecontact/test/io/openems/edge/evcs/keba/kecontact/ReadWorkerTest.java @@ -0,0 +1,72 @@ +package io.openems.edge.evcs.keba.kecontact; + +import static org.junit.Assert.assertEquals; + +import java.time.LocalDateTime; + +import org.junit.Test; + +public class ReadWorkerTest { + + private LocalDateTime now = LocalDateTime.now(); + + @Test + public void testAllReportsDue() { + LocalDateTime lastReport1 = LocalDateTime.MIN; + LocalDateTime lastReport2 = LocalDateTime.MIN; + LocalDateTime lastReport3 = LocalDateTime.MIN; + + int result = ReadWorker.getCycleTimeLogic(lastReport1, lastReport2, lastReport3, this.now); + assertEquals(0, result); + } + + @Test + public void testNoReportsDue() { + LocalDateTime lastReport1 = this.now.minusSeconds(5); + LocalDateTime lastReport2 = this.now.minusSeconds(5); + LocalDateTime lastReport3 = this.now.minusSeconds(5); + + int result = ReadWorker.getCycleTimeLogic(lastReport1, lastReport2, lastReport3, this.now); + assertEquals(5000, result); + } + + @Test + public void testOneReportDue() { + LocalDateTime lastReport1 = this.now.minusHours(1); + LocalDateTime lastReport2 = this.now.minusSeconds(1); + LocalDateTime lastReport3 = this.now.minusSeconds(1); + + int result = ReadWorker.getCycleTimeLogic(lastReport1, lastReport2, lastReport3, this.now); + assertEquals(0, result); + } + + @Test + public void testReportsInFuture() { + LocalDateTime lastReport1 = this.now.plusSeconds(5); + LocalDateTime lastReport2 = this.now.plusSeconds(5); + LocalDateTime lastReport3 = this.now.plusSeconds(5); + + int result = ReadWorker.getCycleTimeLogic(lastReport1, lastReport2, lastReport3, this.now); + assertEquals(15000, result); + } + + @Test + public void testReportsFarInFuture() { + LocalDateTime lastReport1 = LocalDateTime.MAX; + LocalDateTime lastReport2 = LocalDateTime.MAX; + LocalDateTime lastReport3 = LocalDateTime.MAX; + + int result = ReadWorker.getCycleTimeLogic(lastReport1, lastReport2, lastReport3, this.now); + assertEquals(0, result); + } + + @Test + public void testEdgeCaseReportNearNow() { + LocalDateTime lastReport1 = this.now.minusSeconds(Report.REPORT1.getRequestSeconds() - 1); + LocalDateTime lastReport2 = this.now.minusSeconds(Report.REPORT2.getRequestSeconds() - 1); + LocalDateTime lastReport3 = this.now.minusSeconds(Report.REPORT3.getRequestSeconds() - 1); + + int result = ReadWorker.getCycleTimeLogic(lastReport1, lastReport2, lastReport3, this.now); + assertEquals(1000, result); + } +} \ No newline at end of file diff --git a/io.openems.edge.goodwe/doc/GoodWeSerialNrRule.png b/io.openems.edge.goodwe/doc/GoodWeSerialNrRule.png new file mode 100644 index 00000000000..1e55c49d82c Binary files /dev/null and b/io.openems.edge.goodwe/doc/GoodWeSerialNrRule.png differ diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/AbstractGoodWeEtCharger.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/AbstractGoodWeEtCharger.java index cf6d669406e..97eb0bcba86 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/AbstractGoodWeEtCharger.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/AbstractGoodWeEtCharger.java @@ -88,17 +88,11 @@ private void updateState() { var goodWe = this.getEssOrBatteryInverter(); Boolean hasNoDcPv = null; if (goodWe != null) { - switch (goodWe.getGoodweType().getSeries()) { - case BT: - hasNoDcPv = true; - break; - case ET: - hasNoDcPv = false; - break; - case UNDEFINED: - hasNoDcPv = null; - break; - } + hasNoDcPv = switch (goodWe.getGoodweType().getSeries()) { + case BT -> true; + case ET, ETT -> false; + case UNDEFINED -> null; + }; } this._setHasNoDcPv(hasNoDcPv); } diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java index 3fcfec14317..e6765b39ca9 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java @@ -1195,10 +1195,12 @@ protected final ModbusProtocol defineModbusProtocol() { ); /* - * Handles different GoodWe Types. + * Handle different GoodWe Types. * - * Register 35011: GoodWeType as String (Not supported for GoodWe 20 & 30) - * Register 35003: Serial number as String (Fallback for GoodWe 20 & 30) + * GoodweType Firmware is differing from Type ET-Plus to ETT. + * + * Register 35011: GoodWeType as String (Not supported for GoodWe 20 & 30 - ETT) + * Register 35003: Serial number as String (Fallback for GoodWe 20 & 30 - ETT) */ readElementOnce(protocol, ModbusUtils::retryOnNull, new StringWordElement(35011, 5)) // .thenAccept(value -> { @@ -1210,8 +1212,30 @@ protected final ModbusProtocol defineModbusProtocol() { TypeUtils.getAsType(OpenemsType.STRING, value)); if (resultFromString != GoodWeType.UNDEFINED) { + + /* + * ET-Plus + */ this.logInfo(this.log, "Identified " + resultFromString.getName()); this._setGoodweType(resultFromString); + + // Handles different ET-Plus DSP versions + ModbusUtils.readElementOnce(protocol, ModbusUtils::retryOnNull, new UnsignedWordElement(35016)) // + .thenAccept(dspVersion -> { + try { + if (dspVersion >= 5) { + this.handleDspVersion5(protocol); + } + if (dspVersion >= 6) { + this.handleDspVersion6(protocol); + } + if (dspVersion >= 7) { + this.handleDspVersion7(protocol); + } + } catch (OpenemsException e) { + this.logError(this.log, "Unable to add task for modbus protocol"); + } + }); return; } @@ -1220,46 +1244,23 @@ protected final ModbusProtocol defineModbusProtocol() { */ readElementOnce(protocol, ModbusUtils::retryOnNull, new StringWordElement(35003, 8)) // .thenAccept(serialNr -> { + final var hardwareType = getGoodWeTypeFromSerialNr(serialNr); try { this._setGoodweType(hardwareType); + this.handleDspVersion5(protocol); + this.handleDspVersion6(protocol); + this.handleDspVersion7(protocol); if (hardwareType == GoodWeType.FENECON_FHI_20_DAH || hardwareType == GoodWeType.FENECON_FHI_29_9_DAH) { this.handleMultipleStringChargers(protocol); } - } catch (OpenemsException e) { this.logError(this.log, "Unable to add charger tasks for modbus protocol"); } }); }); - // Handles different DSP versions - readElementOnce(protocol, ModbusUtils::retryOnNull, new UnsignedWordElement(35016)) // - .thenAccept(dspVersion -> { - try { - - // GoodWe 30 has DspFmVersionMaster=0 & DspBetaVersion=80 - if (dspVersion == 0) { - this.handleDspVersion5(protocol); - this.handleDspVersion6(protocol); - this.handleDspVersion7(protocol); - return; - } - if (dspVersion >= 5) { - this.handleDspVersion5(protocol); - } - if (dspVersion >= 6) { - this.handleDspVersion6(protocol); - } - if (dspVersion >= 7) { - this.handleDspVersion7(protocol); - } - } catch (OpenemsException e) { - this.logError(this.log, "Unable to add task for modbus protocol"); - } - }); - return protocol; } @@ -1329,7 +1330,7 @@ protected static GoodWeType getGoodWeTypeFromSerialNr(String serialNr) { /** * Handle multiple string chargers. - * + * *

* For MPPT connectors e.g. two string on one MPPT the power information is * spread over several registers that should be read as complete blocks. diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/enums/GoodWeType.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/enums/GoodWeType.java index 32e2a1cec1a..c927165fb09 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/enums/GoodWeType.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/enums/GoodWeType.java @@ -15,19 +15,17 @@ public enum GoodWeType implements OptionsEnum { GOODWE_10K_ET(20, "GoodWe GW10K-ET", Series.ET, 25), // GOODWE_8K_ET(21, "GoodWe GW8K-ET", Series.ET, 25), // GOODWE_5K_ET(22, "GoodWe GW5K-ET", Series.ET, 25), // - FENECON_FHI_10_DAH(30, "FENECON FHI 10 DAH", Series.ET, 25, position2Filter("10"), + FENECON_FHI_10_DAH(30, "FENECON FHI 10 DAH", Series.ET, 25, serialNrFilter("010K", "ETU"), (batteryType) -> batteryType != BatteryFeneconHomeHardwareType.BATTERY_52), // - FENECON_FHI_20_DAH(120, "FENECON FHI 20 DAH", Series.ET, 50, position2Filter("20"), + FENECON_FHI_20_DAH(120, "FENECON FHI 20 DAH", Series.ETT, 50, serialNrFilter("020K", "ETT"), (batteryType) -> batteryType != BatteryFeneconHomeHardwareType.BATTERY_64), // - FENECON_FHI_29_9_DAH(130, "FENECON FHI 30 DAH", Series.ET, 50, home30Filter("29K9", "30"), + FENECON_FHI_29_9_DAH(130, "FENECON FHI 30 DAH", Series.ETT, 50, home30Filter("29K9", "030K"), (batteryType) -> batteryType != BatteryFeneconHomeHardwareType.BATTERY_64); // public static enum Series { - UNDEFINED, BT, ET; + UNDEFINED, BT, ET, ETT; } - // TODO: Change logic of isValidHomeBattery to invalidBattery - private final int value; private final String option; private final Series series; @@ -100,8 +98,28 @@ public static ThrowingFunction position2Filter(Strin */ public static ThrowingFunction home30Filter(String... match) { return serialNr -> Stream.of(match) // - .filter(t -> t.equals(serialNr.substring(2, 4)) || serialNr.substring(1, 5).contains(t)) // + .filter(t -> { + try { + return serialNrFilter(t, "ETT").apply(serialNr); + } catch (Exception e) { + return false; + } + }) // .findFirst() // .isPresent(); } + + /** + * GoodWe serial number filter. + * + *

+ * Check if a serialNr matches a given string at the common position. + * + * @param ratedPower rated power of the inverter + * @param seriesCode internal inverter model series code + * @return filter function + */ + public static ThrowingFunction serialNrFilter(String ratedPower, String seriesCode) { + return serialNr -> serialNr.substring(1, 5).equals(ratedPower) && serialNr.substring(5, 8).equals(seriesCode); + } } \ No newline at end of file diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImpl.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImpl.java index f8ec005a0ed..6e9a4fac228 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImpl.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImpl.java @@ -1,11 +1,14 @@ package io.openems.edge.goodwe.gridmeter; +import static io.openems.common.types.OpenemsType.INTEGER; import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.INVERT; import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_1; import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_2; import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_MINUS_2; import static io.openems.edge.bridge.modbus.api.ModbusUtils.readElementOnce; +import java.util.function.Supplier; + import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -27,7 +30,6 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; import io.openems.common.types.MeterType; -import io.openems.common.types.OpenemsType; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ElementToChannelConverter; @@ -41,6 +43,7 @@ import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask; import io.openems.edge.bridge.modbus.api.task.FC6WriteRegisterTask; import io.openems.edge.common.channel.ChannelUtils; +import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.modbusslave.ModbusSlave; @@ -127,7 +130,6 @@ protected void deactivate() { @Override protected ModbusProtocol defineModbusProtocol() { var protocol = new ModbusProtocol(this, // - // States new FC3ReadRegistersTask(36003, Priority.LOW, m(new UnsignedWordElement(36003)).build().onUpdateCallback((value) -> { @@ -136,7 +138,7 @@ protected ModbusProtocol defineModbusProtocol() { m(GoodWeGridMeter.ChannelId.HAS_NO_METER, new UnsignedWordElement(36004), new ElementToChannelConverter(value -> { - Integer intValue = TypeUtils.getAsType(OpenemsType.INTEGER, value); + Integer intValue = TypeUtils.getAsType(INTEGER, value); if (intValue != null) { switch (intValue) { case 0: @@ -206,11 +208,14 @@ private void handleDspVersion4(ModbusProtocol protocol) { m(ElectricityMeter.ChannelId.VOLTAGE_L3, new UnsignedWordElement(36054), this.ignoreZeroAndScaleFactor2), // m(ElectricityMeter.ChannelId.CURRENT_L1, new UnsignedWordElement(36055), - this.ignoreZeroAndScaleFactor2), // + ElementToChannelConverter.chain(this.ignoreZeroAndScaleFactor2, // + createAdjustCurrentSign(this.getActivePowerL1Channel()::getNextValue))), // m(ElectricityMeter.ChannelId.CURRENT_L2, new UnsignedWordElement(36056), - this.ignoreZeroAndScaleFactor2), // + ElementToChannelConverter.chain(this.ignoreZeroAndScaleFactor2, // + createAdjustCurrentSign(this.getActivePowerL2Channel()::getNextValue))), // m(ElectricityMeter.ChannelId.CURRENT_L3, new UnsignedWordElement(36057), - this.ignoreZeroAndScaleFactor2))); // + ElementToChannelConverter.chain(this.ignoreZeroAndScaleFactor2, // + createAdjustCurrentSign(this.getActivePowerL3Channel()::getNextValue))))); // } private void handleExternalMeter(ModbusProtocol protocol) { @@ -318,7 +323,7 @@ protected void convertMeterConnectStatus(Integer value) { /** * Get the connection value depending on the phase. - * + * *

* The information of each phase connection is part of a hex. The part of the * given phase will be returned. @@ -406,4 +411,24 @@ public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { ModbusSlaveNatureTable.of(GoodWeGridMeter.class, accessMode, 100).build() // ); } + + /** + * Creates an {@link ElementToChannelConverter} for + * {@link ElectricityMeter.ChannelId#CURRENT_L1}, + * {@link ElectricityMeter.ChannelId#CURRENT_L2} and + * {@link ElectricityMeter.ChannelId#CURRENT_L3} that adjusts the sign to that + * given by a supplier. + * + * @param getActivePowerNextValue {@link Supplier} for a value with a sign that + * should be copied + * @return the {@link ElementToChannelConverter} + */ + protected static ElementToChannelConverter createAdjustCurrentSign( + Supplier> getActivePowerNextValue) { + return new ElementToChannelConverter(value -> { + var activePower = getActivePowerNextValue.get().orElse(0); + Integer intValue = TypeUtils.getAsType(INTEGER, value); + return Math.abs(intValue) * Integer.signum(activePower); + }); + } } diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/common/TestStatic.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/common/TestStatic.java index 65fe2da9a22..2889a5135ff 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/common/TestStatic.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/common/TestStatic.java @@ -23,13 +23,12 @@ public void testGetHardwareTypeFromSerialNr() { assertNotEquals(GoodWeType.FENECON_FHI_20_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("9010KETT22AW0004")); assertEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("9030KETT228W0004")); + assertEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("129K9ETT231W0159")); assertNotEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("9020KETT228W0004")); - assertEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("929K9ETT231W0159")); assertNotEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("929KETT231W0159")); assertNotEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("928K9ETT231W0159")); - assertEquals(GoodWeType.FENECON_FHI_29_9_DAH, AbstractGoodWe.getGoodWeTypeFromSerialNr("929K9ETT231W0160")); - assertEquals(GoodWeType.UNDEFINED, AbstractGoodWe.getGoodWeTypeFromSerialNr("9040KETT228W0004")); + assertEquals(GoodWeType.UNDEFINED, AbstractGoodWe.getGoodWeTypeFromSerialNr("9036KETT228W0004")); assertEquals(GoodWeType.UNDEFINED, AbstractGoodWe.getGoodWeTypeFromSerialNr("9000KETT228W0004")); assertEquals(GoodWeType.UNDEFINED, AbstractGoodWe.getGoodWeTypeFromSerialNr("ET2")); assertEquals(GoodWeType.UNDEFINED, AbstractGoodWe.getGoodWeTypeFromSerialNr("")); diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImplTest.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImplTest.java index 79d1e0cf4d3..c32d8e8b7f8 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImplTest.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/gridmeter/GoodWeGridMeterImplTest.java @@ -13,10 +13,12 @@ import org.junit.Test; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; +import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.ess.power.api.Phase; +import io.openems.edge.meter.api.ElectricityMeter; public class GoodWeGridMeterImplTest { @@ -88,6 +90,66 @@ public void testMeterConnectStateConverter() throws Exception { assert noResult == 0x000; } + @Test + public void testReadFromModbus() throws Exception { + new ComponentTest(new GoodWeGridMeterImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("setModbus", new DummyModbusBridge("modbus0") // + .withRegisters(36003, 0, 1) // States + .withRegisters(35123, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) // F_GRID_R etc. + .withRegister(35016, 4) // DSP Version + .withRegisters(36005, // + /* ACTIVE_POWER */ -1000 /* L1 */, -1320 /* L2 */, 1610 /* L3 */, // + /* reserved */ 0, 0, 0, 0, 0, // + /* METER_POWER_FACTOR */ 0, // + /* FREQUENCY */ 50) + .withRegisters(36052, // + /* VOLTAGE */ 2000 /* L1 */, 2200 /* L2 */, 2300 /* L3 */, // + /* CURRENT */ 50 /* L1 */, 60 /* L2 */, 70 /* L3 */)) + .activate(MyConfig.create() // + .setId("meter0") // + .setModbusId("modbus0") // + .setGoodWeMeterCategory(SMART_METER) // + .setExternalMeterRatioValueA(0) // + .setExternalMeterRatioValueB(0) // + .build()) // + + .next(new TestCase() // + .output(GoodWeGridMeter.ChannelId.HAS_NO_METER, false) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, 1000) // inverted + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, 1320) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, -1610) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 710)) // + + .next(new TestCase(), 3) // Wait for 36052 + .next(new TestCase() // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 200_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L2, 220_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L3, 230_000) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, 5_000) // + .output(ElectricityMeter.ChannelId.CURRENT_L2, 6_000) // + .output(ElectricityMeter.ChannelId.CURRENT_L3, -7_000)) // inverted + .deactivate(); + } + + @Test + public void testAdjustCurrentSign() { + { + var e2cConverter = GoodWeGridMeterImpl.createAdjustCurrentSign(() -> new Value(null, -5000)); + // postive to negative + assertEquals(-16, e2cConverter.elementToChannel(16)); + // negative stays negative + assertEquals(-16, e2cConverter.elementToChannel(-16)); + } + { + var e2cConverter = GoodWeGridMeterImpl.createAdjustCurrentSign(() -> new Value(null, 5000)); + // positive stays positive + assertEquals(16, e2cConverter.elementToChannel(16)); + // negative to positive + assertEquals(16, e2cConverter.elementToChannel(-16)); + } + } + @Test public void testExternalMeterRatio() throws Exception { new ComponentTest(new GoodWeGridMeterImpl()) // diff --git a/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/gridmeter/KostalPikoGridMeterImplTest.java b/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/gridmeter/KostalPikoGridMeterImplTest.java index ee5e9206bcb..aec78af51d6 100644 --- a/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/gridmeter/KostalPikoGridMeterImplTest.java +++ b/io.openems.edge.kostal.piko/test/io/openems/edge/kostal/piko/gridmeter/KostalPikoGridMeterImplTest.java @@ -16,8 +16,10 @@ public void test() throws Exception { .activate(MyConfig.create() // .setId("meter0") // .setCoreId("core0") // - .build()) // - ; + .build()); // + // TODO This does not work because this.worker == null + // .next(new TestCase()) // + // deactivate(); } } diff --git a/io.openems.edge.meter.abb/test/io/openems/edge/meter/abb/b32/MeterAbbB23ImplTest.java b/io.openems.edge.meter.abb/test/io/openems/edge/meter/abb/b32/MeterAbbB23ImplTest.java index 0285f714059..367d1f97dd5 100644 --- a/io.openems.edge.meter.abb/test/io/openems/edge/meter/abb/b32/MeterAbbB23ImplTest.java +++ b/io.openems.edge.meter.abb/test/io/openems/edge/meter/abb/b32/MeterAbbB23ImplTest.java @@ -1,17 +1,16 @@ package io.openems.edge.meter.abb.b32; -import java.lang.reflect.InvocationTargetException; - import org.junit.Test; import io.openems.common.types.MeterType; +import io.openems.common.utils.ReflectionUtils.ReflectionException; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; public class MeterAbbB23ImplTest { - @Test(expected = InvocationTargetException.class) + @Test(expected = ReflectionException.class) public void test() throws Exception { new ComponentTest(new MeterAbbB23Impl()) // .addReference("cm", new DummyConfigurationAdmin()) // # diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/asymmetric/reacting/SimulatorEssAsymmetricReactingImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/asymmetric/reacting/SimulatorEssAsymmetricReactingImplTest.java index 8a2656a7343..35c7a7f745c 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/asymmetric/reacting/SimulatorEssAsymmetricReactingImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/asymmetric/reacting/SimulatorEssAsymmetricReactingImplTest.java @@ -8,22 +8,19 @@ import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.ess.test.DummyPower; import io.openems.edge.ess.test.ManagedSymmetricEssTest; -import io.openems.edge.simulator.datasource.csv.direct.SimulatorDatasourceCsvDirectImpl; +import io.openems.edge.simulator.datasource.csv.direct.SimulatorDatasourceCsvDirectImplTest; public class SimulatorEssAsymmetricReactingImplTest { - private static final String ESS_ID = "ess0"; - private static final String DATASOURCE_ID = "datasource0"; - @Test public void test() throws OpenemsException, Exception { new ManagedSymmetricEssTest(new SimulatorEssAsymmetricReactingImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("datasource", new SimulatorDatasourceCsvDirectImpl()) // + .addReference("datasource", SimulatorDatasourceCsvDirectImplTest.create("datasource0", "123")) // .addReference("power", new DummyPower()) // .activate(MyConfig.create() // - .setId(ESS_ID) // - .setDatasourceId(DATASOURCE_ID) // + .setId("ess0") // + .setDatasourceId("datasource0") // .setCapacity(10_000) // .setMaxApparentPower(10_000) // .setInitialSoc(50) // diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/singlephase/reacting/SimulatorEssSinglePhaseReactingImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/singlephase/reacting/SimulatorEssSinglePhaseReactingImplTest.java index d00be5bce1c..38be1552e53 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/singlephase/reacting/SimulatorEssSinglePhaseReactingImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/singlephase/reacting/SimulatorEssSinglePhaseReactingImplTest.java @@ -9,7 +9,7 @@ import io.openems.edge.ess.api.SinglePhase; import io.openems.edge.ess.test.DummyPower; import io.openems.edge.ess.test.ManagedSymmetricEssTest; -import io.openems.edge.simulator.datasource.csv.direct.SimulatorDatasourceCsvDirectImpl; +import io.openems.edge.simulator.datasource.csv.direct.SimulatorDatasourceCsvDirectImplTest; public class SimulatorEssSinglePhaseReactingImplTest { @@ -20,7 +20,7 @@ public class SimulatorEssSinglePhaseReactingImplTest { public void test() throws OpenemsException, Exception { new ManagedSymmetricEssTest(new SimulatorEssSinglePhaseReactingImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("datasource", new SimulatorDatasourceCsvDirectImpl()) // + .addReference("datasource", SimulatorDatasourceCsvDirectImplTest.create("datasource0", "123")) // .addReference("power", new DummyPower()) // .activate(MyConfig.create() // .setId(ESS_ID) // diff --git a/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/DummyRecordWorkerFactory.java b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/DummyRecordWorkerFactory.java index b4af1e2e5a3..90492eead0f 100644 --- a/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/DummyRecordWorkerFactory.java +++ b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/DummyRecordWorkerFactory.java @@ -1,20 +1,18 @@ package io.openems.edge.timedata.rrd4j; -import java.lang.reflect.InvocationTargetException; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentServiceObjects; -import io.openems.common.utils.ReflectionUtils; +import io.openems.common.utils.ReflectionUtils.ReflectionException; import io.openems.edge.common.component.ComponentManager; public class DummyRecordWorkerFactory extends RecordWorkerFactory { - public DummyRecordWorkerFactory(ComponentManager componentManager) - throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + public DummyRecordWorkerFactory(ComponentManager componentManager) throws ReflectionException { super(); - ReflectionUtils.setAttribute(RecordWorkerFactory.class, this, "cso", - new DummyRecordWorkerCso(componentManager)); + setAttributeViaReflection(this, "cso", new DummyRecordWorkerCso(componentManager)); } private static class DummyRecordWorkerCso implements ComponentServiceObjects { @@ -29,11 +27,7 @@ public DummyRecordWorkerCso(ComponentManager componentManager) { @Override public RecordWorker getService() { final var worker = new RecordWorker(); - try { - ReflectionUtils.setAttribute(RecordWorker.class, worker, "componentManager", this.componentManager); - } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } + setAttributeViaReflection(worker, "componentManager", this.componentManager); return worker; } diff --git a/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jReadHandlerTest.java b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jReadHandlerTest.java index f0c0eab0d27..c9135ed3ab4 100644 --- a/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jReadHandlerTest.java +++ b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jReadHandlerTest.java @@ -1,5 +1,6 @@ package io.openems.edge.timedata.rrd4j; +import static io.openems.common.utils.ReflectionUtils.setAttributeViaReflection; import static java.util.stream.Collectors.toMap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -27,7 +28,6 @@ import io.openems.common.timedata.Resolution; import io.openems.common.types.ChannelAddress; import io.openems.common.types.OpenemsType; -import io.openems.common.utils.ReflectionUtils; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.test.AbstractDummyOpenemsComponent; @@ -80,9 +80,9 @@ public void setUp() throws Exception { final var versionHandler = new VersionHandler(); versionHandler.bindVersion(version3); - ReflectionUtils.setAttribute(Rrd4jReadHandler.class, this.readHandler, "componentManager", dcm); - ReflectionUtils.setAttribute(Rrd4jReadHandler.class, this.readHandler, "rrd4jSupplier", rrd4jSupplier); - ReflectionUtils.setAttribute(Rrd4jSupplier.class, rrd4jSupplier, "versionHandler", versionHandler); + setAttributeViaReflection(this.readHandler, "componentManager", dcm); + setAttributeViaReflection(this.readHandler, "rrd4jSupplier", rrd4jSupplier); + setAttributeViaReflection(rrd4jSupplier, "versionHandler", versionHandler); } diff --git a/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApi.java b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApi.java index b053d9c2757..6f3c4b3e727 100644 --- a/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApi.java +++ b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApi.java @@ -1,13 +1,17 @@ package io.openems.edge.timeofusetariff.api.utils; -import static io.openems.common.utils.JsonUtils.getAsDouble; -import static io.openems.common.utils.JsonUtils.getAsJsonObject; -import static io.openems.common.utils.JsonUtils.parseToJsonObject; +import static io.openems.common.utils.XmlUtils.getXmlRootDocument; +import static io.openems.common.utils.XmlUtils.stream; import java.io.IOException; +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.utils.XmlUtils; import io.openems.edge.common.currency.Currency; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -18,29 +22,29 @@ *

* Day ahead prices retrieved from ENTSO-E are usually in EUR and might have to * be converted to the user's currency using the exchange rates provided by - * Exchange Rate API. For more information on the ExchangeRate API, visit: - * https://exchangerate.host/#/docs + * European Central Bank. */ // TODO this should be extracted to a Exchange-Rate API + Provider public class ExchangeRateApi { - private static final String BASE_URL = "http://api.exchangerate.host/live?access_key=%s&source=%s¤cies=%s"; + private static final String ECB_URL = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"; + + // ECB gives exchange rates based on the EUR. + private static final Currency BASE_CURRENCY = Currency.EUR; private static final OkHttpClient client = new OkHttpClient(); /** - * Fetches the exchange rate from exchangerate.host. + * Fetches the exchange rate from ECB API. * - * @param accessKey personal API access key. - * @param source the source currency (e.g. EUR) - * @param target the target currency (e.g. SEK) - * @param orElse the default value + * @param source the source currency (e.g. EUR) + * @param target the target currency (e.g. SEK) + * @param orElse the default value * @return the exchange rate. */ - public static double getExchangeRateOrElse(String accessKey, String source, Currency target, double orElse) { + public static double getExchangeRateOrElse(String source, Currency target, double orElse) { try { - return getExchangeRate(accessKey, source, target); + return getExchangeRate(source, target); } catch (Exception e) { e.printStackTrace(); return orElse; @@ -48,27 +52,28 @@ public static double getExchangeRateOrElse(String accessKey, String source, Curr } /** - * Fetches the exchange rate from exchangerate.host. + * Fetches the exchange rate from ECB API. * - * @param accessKey personal API access key. - * @param source the source currency (e.g. EUR) - * @param target the target currency (e.g. SEK) + * @param source the source currency (e.g. EUR) + * @param target the target currency (e.g. SEK) * @return the exchange rate. - * @throws IOException on error. - * @throws OpenemsNamedException on error + * @throws IOException on error + * @throws OpenemsNamedException on error + * @throws SAXException on error + * @throws ParserConfigurationException on error */ - public static double getExchangeRate(String accessKey, String source, Currency target) - throws IOException, OpenemsNamedException { + public static double getExchangeRate(String source, Currency target) + throws IOException, OpenemsNamedException, ParserConfigurationException, SAXException { if (target == Currency.UNDEFINED) { throw new OpenemsException("Global Currency is UNDEFINED. Please configure it in Core.Meta component"); } - if (target.name().equals(source)) { + if (target.name().equals(source) || target.equals(BASE_CURRENCY)) { return 1.; // No need to fetch exchange rate from API } var request = new Request.Builder() // - .url(String.format(BASE_URL, accessKey, source, target.name())) // + .url(ECB_URL) // .build(); try (var response = client.newCall(request).execute()) { @@ -76,25 +81,34 @@ public static double getExchangeRate(String accessKey, String source, Currency t throw new IOException("Failed to fetch exchange rate. HTTP status code: " + response.code()); } - return parseResponse(response.body().string(), source, target); + return parseResponse(response.body().string(), target); } } /** - * Parses the response string from exchangerate.host. + * Parses the response string from ECB API. * * @param response the response string - * @param source the source currency (e.g. EUR) * @param target the target currency (e.g. SEK) * @return the exchange rate. - * @throws OpenemsNamedException on error. + * @throws IOException on error. + * @throws SAXException on error. + * @throws ParserConfigurationException on error. */ - protected static double parseResponse(String response, String source, Currency target) - throws OpenemsNamedException { - var json = parseToJsonObject(response); - var quotes = getAsJsonObject(json, "quotes"); - var result = getAsDouble(quotes, source + target.name()); - return result; + protected static double parseResponse(String response, Currency target) + throws ParserConfigurationException, SAXException, IOException { + var root = getXmlRootDocument(response); + return stream(root) // + .filter(n -> n.getNodeName() == "Cube") // Filter all elements + .flatMap(XmlUtils::stream) // Stream the child nodes of each + .filter(n -> n.getNodeName().equals("Cube")) // + .flatMap(XmlUtils::stream) // + .filter(n -> n.getNodeName().equals("Cube")) // + .filter(n -> n.getAttributes().getNamedItem("currency").getNodeValue().equals(target.name())) // + .map(n -> n.getAttributes().getNamedItem("rate").getNodeValue()) // + .mapToDouble(Double::parseDouble) // + .findFirst() // + .getAsDouble(); } } diff --git a/io.openems.edge.timeofusetariff.api/test/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApiTest.java b/io.openems.edge.timeofusetariff.api/test/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApiTest.java index 81657b127c0..7d0ee419286 100644 --- a/io.openems.edge.timeofusetariff.api/test/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApiTest.java +++ b/io.openems.edge.timeofusetariff.api/test/io/openems/edge/timeofusetariff/api/utils/ExchangeRateApiTest.java @@ -5,40 +5,72 @@ import java.io.IOException; +import javax.xml.parsers.ParserConfigurationException; + import org.junit.Ignore; import org.junit.Test; +import org.xml.sax.SAXException; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.oem.DummyOpenemsEdgeOem; import io.openems.edge.common.currency.Currency; public class ExchangeRateApiTest { private static final String RESPONSE = """ - { - "success": true, - "terms": "https://currencylayer.com/terms", - "privacy": "https://currencylayer.com/privacy", - "timestamp": 1699605243, - "source": "EUR", - "quotes": { - "EURSEK": 11.649564 - } - } - """; + + Reference rates + + European Central Bank + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """; // Remove '@Ignore' tag to test this API call. @Ignore @Test - public void testGetExchangeRate() throws IOException, OpenemsNamedException { - var oem = new DummyOpenemsEdgeOem(); - var rate = getExchangeRate(oem.getExchangeRateAccesskey(), "EUR", Currency.SEK); + public void testGetExchangeRate() throws IOException, OpenemsNamedException, ParserConfigurationException, SAXException { + var rate = getExchangeRate("EUR", Currency.SEK); System.out.println(rate); } @Test - public void testParseResponse() throws OpenemsNamedException { - var rate = ExchangeRateApi.parseResponse(RESPONSE, "EUR", Currency.SEK); - assertEquals(11.649564, rate, 0.0001); + public void testParseResponse() + throws OpenemsNamedException, ParserConfigurationException, SAXException, IOException { + var rate = ExchangeRateApi.parseResponse(RESPONSE, Currency.SEK); + assertEquals(11.4475, rate, 0.0001); } } diff --git a/io.openems.edge.timeofusetariff.entsoe/readme.adoc b/io.openems.edge.timeofusetariff.entsoe/readme.adoc index aea3aee6223..83b1738c11c 100644 --- a/io.openems.edge.timeofusetariff.entsoe/readme.adoc +++ b/io.openems.edge.timeofusetariff.entsoe/readme.adoc @@ -6,6 +6,6 @@ To request a (free) authentication token, please see chapter "2. Authentication Prices retrieved from ENTSO-E are subsequently converted to the user's currency (defined in Core.Meta Component) using the Exchange Rates API. -For detailed information about the Exchange Rates API, please refer to: https://exchangerate.host/#/docs +For detailed information about the Exchange Rates API, please refer to: https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.timeofusetariff.entsoe[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Config.java b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Config.java index 73a451dee78..e0f5ac2ac9b 100644 --- a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Config.java +++ b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Config.java @@ -21,9 +21,6 @@ @AttributeDefinition(name = "Security Token", description = "Security token for the ENTSO-E Transparency Platform", type = AttributeType.PASSWORD) String securityToken() default ""; - @AttributeDefinition(name = "Exchangerate.host API Access Key", description = "Access key for Exchangerate.host: Please register at https://exchangerate.host/ to get your personal access key", type = AttributeType.PASSWORD) - String exchangerateAccesskey() default ""; - @AttributeDefinition(name = "Bidding Zone", description = "Zone corresponding to the customer's location") BiddingZone biddingZone(); diff --git a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/TouEntsoeImpl.java b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/TouEntsoeImpl.java index f2fd0be8c39..fda5777bdd5 100644 --- a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/TouEntsoeImpl.java +++ b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/TouEntsoeImpl.java @@ -61,7 +61,6 @@ public class TouEntsoeImpl extends AbstractOpenemsComponent implements TouEntsoe private Config config = null; private String securityToken = null; - private String exchangerateAccesskey = null; private ScheduledFuture future = null; public TouEntsoeImpl() { @@ -89,11 +88,6 @@ private void activate(ComponentContext context, Config config) { return; } - this.exchangerateAccesskey = definedOrElse(config.exchangerateAccesskey(), this.oem.getExchangeRateAccesskey()); - if (this.exchangerateAccesskey == null) { - this.logError(this.log, "Please configure personal Access key to access Exchange rate host API"); - return; - } this.config = config; // React on updates to Currency. @@ -124,7 +118,6 @@ private synchronized void scheduleTask(long seconds) { private final Runnable task = () -> { var token = this.securityToken; - var exchangerateAccesskey = this.exchangerateAccesskey; var areaCode = this.config.biddingZone().code; var fromDate = ZonedDateTime.now().truncatedTo(ChronoUnit.HOURS); var toDate = fromDate.plusDays(1); @@ -134,8 +127,7 @@ private synchronized void scheduleTask(long seconds) { final var result = EntsoeApi.query(token, areaCode, fromDate, toDate); final var entsoeCurrency = parseCurrency(result); final var globalCurrency = this.meta.getCurrency(); - final double exchangeRate = getExchangeRateOrElse(// - exchangerateAccesskey, entsoeCurrency, globalCurrency, 1.); + final double exchangeRate = getExchangeRateOrElse(entsoeCurrency, globalCurrency, 1.); // Parse the response for the prices this.prices.set(parsePrices(result, exchangeRate)); diff --git a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Utils.java b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Utils.java index 4e573ee85a9..b3b3a010a5b 100644 --- a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Utils.java +++ b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Utils.java @@ -1,6 +1,7 @@ package io.openems.edge.timeofusetariff.entsoe; import static io.openems.common.utils.XmlUtils.stream; +import static io.openems.common.utils.XmlUtils.getXmlRootDocument; import static java.lang.Double.parseDouble; import static java.lang.Integer.parseInt; @@ -72,15 +73,6 @@ protected static TimeOfUsePrices parsePrices(String xml, double exchangeRate) return TimeOfUsePrices.from(result); } - protected static Element getXmlRootDocument(String xml) - throws ParserConfigurationException, SAXException, IOException { - var dbFactory = DocumentBuilderFactory.newInstance(); - var dBuilder = dbFactory.newDocumentBuilder(); - var is = new InputSource(new StringReader(xml)); - var doc = dBuilder.parse(is); - return doc.getDocumentElement(); - } - protected static ImmutableTable parseXml(Element root, double exchangeRate) { var result = ImmutableTable.builder(); stream(root) // diff --git a/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/MyConfig.java b/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/MyConfig.java index 1bdbe072264..c3dbb4ec27a 100644 --- a/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/MyConfig.java +++ b/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/MyConfig.java @@ -8,7 +8,6 @@ public class MyConfig extends AbstractComponentConfig implements Config { protected static class Builder { private String id; private String securityToken; - private String exchangerateAccesskey; private BiddingZone biddingZone; private Builder() { @@ -24,11 +23,6 @@ public Builder setSecurityToken(String securityToken) { return this; } - public Builder setExchangerateAccesskey(String exchangerateAccesskey) { - this.exchangerateAccesskey = exchangerateAccesskey; - return this; - } - public Builder setBiddingZone(BiddingZone biddingZone) { this.biddingZone = biddingZone; return this; @@ -60,11 +54,6 @@ public String securityToken() { return this.builder.securityToken; } - @Override - public String exchangerateAccesskey() { - return this.builder.exchangerateAccesskey; - } - @Override public BiddingZone biddingZone() { return this.builder.biddingZone; diff --git a/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/TouEntsoeTest.java b/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/TouEntsoeTest.java index 8bcea20e8ce..3aa234085ab 100644 --- a/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/TouEntsoeTest.java +++ b/io.openems.edge.timeofusetariff.entsoe/test/io/openems/edge/timeofusetariff/entsoe/TouEntsoeTest.java @@ -21,7 +21,6 @@ public void test() throws Exception { .activate(MyConfig.create() // .setId("tou0") // .setSecurityToken("") // - .setExchangerateAccesskey("") // .setBiddingZone(BiddingZone.GERMANY) // .build()); } diff --git a/io.openems.edge.timeofusetariff.groupe/readme.adoc b/io.openems.edge.timeofusetariff.groupe/readme.adoc index 2d36804c53d..9c6552c4410 100644 --- a/io.openems.edge.timeofusetariff.groupe/readme.adoc +++ b/io.openems.edge.timeofusetariff.groupe/readme.adoc @@ -4,6 +4,6 @@ This implementation uses the Groupe-E platform to receive day-ahead quarterly pr Prices retrieved from Groupe-E are subsequently converted to the user's currency (defined in Core.Meta Component) using the Exchange Rates API. -For detailed information about the Exchange Rates API, please refer to: https://exchangerate.host/#/docs +For detailed information about the Exchange Rates API, please refer to: https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.timeofusetariff.groupe[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/Config.java b/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/Config.java index 4204fc86375..ea6cb86adfe 100644 --- a/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/Config.java +++ b/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/Config.java @@ -1,7 +1,6 @@ package io.openems.edge.timeofusetariff.groupe; import org.osgi.service.metatype.annotations.AttributeDefinition; -import org.osgi.service.metatype.annotations.AttributeType; import org.osgi.service.metatype.annotations.ObjectClassDefinition; @ObjectClassDefinition(// @@ -17,9 +16,6 @@ @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") boolean enabled() default true; - - @AttributeDefinition(name = "Exchangerate.host API Access Key", description = "Access key for Exchangerate.host: Please register at https://exchangerate.host/ to get your personal access key", type = AttributeType.PASSWORD) - String exchangerateAccesskey() default ""; String webconsole_configurationFactory_nameHint() default "Time-Of-Use Tariff GroupeE [{id}]"; } diff --git a/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImpl.java b/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImpl.java index b05f68bc2c2..52d733a6a17 100644 --- a/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImpl.java +++ b/io.openems.edge.timeofusetariff.groupe/src/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImpl.java @@ -3,11 +3,10 @@ import static io.openems.common.utils.JsonUtils.getAsDouble; import static io.openems.common.utils.JsonUtils.getAsString; import static io.openems.common.utils.JsonUtils.parseToJsonArray; -import static io.openems.common.utils.StringUtils.definedOrElse; +import static io.openems.edge.timeofusetariff.api.utils.ExchangeRateApi.getExchangeRateOrElse; import static io.openems.edge.timeofusetariff.api.utils.TimeOfUseTariffUtils.generateDebugLog; import static java.util.Collections.emptyMap; -import java.io.IOException; import java.time.Clock; import java.time.Duration; import java.time.ZonedDateTime; @@ -28,8 +27,6 @@ import org.slf4j.LoggerFactory; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.exceptions.OpenemsException; -import io.openems.common.oem.OpenemsEdgeOem; import io.openems.common.timedata.DurationUnit; import io.openems.edge.bridge.http.api.BridgeHttp; import io.openems.edge.bridge.http.api.BridgeHttp.Endpoint; @@ -47,7 +44,6 @@ import io.openems.edge.common.meta.Meta; import io.openems.edge.timeofusetariff.api.TimeOfUsePrices; import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; -import io.openems.edge.timeofusetariff.api.utils.ExchangeRateApi; @Designate(ocd = Config.class, factory = true) @Component(// @@ -63,15 +59,11 @@ public class TimeOfUseTariffGroupeImpl extends AbstractOpenemsComponent private final Logger log = LoggerFactory.getLogger(TimeOfUseTariffGroupeImpl.class); private final AtomicReference prices = new AtomicReference<>(TimeOfUsePrices.EMPTY_PRICES); - private String exchangerateAccesskey = null; @Reference private BridgeHttpFactory httpBridgeFactory; private BridgeHttp httpBridge; - @Reference - private OpenemsEdgeOem oem; - @Reference private Meta meta; @@ -86,7 +78,7 @@ public TimeOfUseTariffGroupeImpl() { } private final BiConsumer, Value> onCurrencyChange = (a, b) -> { - this.httpBridge = this.httpBridgeFactory.get(); + this.httpBridge.removeAllTimeEndpoints(); this.httpBridge.subscribeTime(new GroupeDelayTimeProvider(this.componentManager.getClock()), // this::createGroupeEndpoint, // this::handleEndpointResponse, // @@ -101,12 +93,6 @@ private void activate(ComponentContext context, Config config) { return; } - this.exchangerateAccesskey = definedOrElse(config.exchangerateAccesskey(), this.oem.getExchangeRateAccesskey()); - if (this.exchangerateAccesskey == null) { - this.logError(this.log, "Please configure personal Access key to access Exchange rate host API"); - return; - } - // React on updates to Currency. this.meta.getCurrencyChannel().onChange(this.onCurrencyChange); @@ -167,19 +153,12 @@ public Delay onSuccessRunDelay(HttpResponse result) { } } - private void handleEndpointResponse(HttpResponse response) throws OpenemsNamedException, IOException { + private void handleEndpointResponse(HttpResponse response) throws OpenemsNamedException { this.channel(TimeOfUseTariffGroupe.ChannelId.HTTP_STATUS_CODE).setNextValue(response.status().code()); final var groupeCurrency = Currency.CHF.name(); // Swiss Franc final var globalCurrency = this.meta.getCurrency(); - final var exchangerateAccesskey = this.exchangerateAccesskey; - if (globalCurrency == Currency.UNDEFINED) { - throw new OpenemsException("Global Currency is UNDEFINED. Please configure it in Core.Meta component"); - } - - final var exchangeRate = globalCurrency.name().equals(groupeCurrency) // - ? 1. // No need to fetch exchange rate from API. - : ExchangeRateApi.getExchangeRate(exchangerateAccesskey, groupeCurrency, globalCurrency); + final double exchangeRate = getExchangeRateOrElse(groupeCurrency, globalCurrency, 1.); // Parse the response for the prices this.prices.set(parsePrices(response.data(), exchangeRate)); diff --git a/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/MyConfig.java b/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/MyConfig.java index 13fa3e31c71..6fcaa6cc93c 100644 --- a/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/MyConfig.java +++ b/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/MyConfig.java @@ -7,7 +7,6 @@ public class MyConfig extends AbstractComponentConfig implements Config { public static class Builder { private String id; - private String exchangerateAccesskey; private Builder() { } @@ -17,11 +16,6 @@ public Builder setId(String id) { return this; } - public Builder setExchangerateAccesskey(String exchangerateAccesskey) { - this.exchangerateAccesskey = exchangerateAccesskey; - return this; - } - public MyConfig build() { return new MyConfig(this); } @@ -43,9 +37,4 @@ private MyConfig(Builder builder) { this.builder = builder; } - @Override - public String exchangerateAccesskey() { - return this.builder.exchangerateAccesskey; - } - } \ No newline at end of file diff --git a/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImplTest.java b/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImplTest.java index 76e1e7fda13..ebd8707bfb8 100644 --- a/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImplTest.java +++ b/io.openems.edge.timeofusetariff.groupe/test/io/openems/edge/timeofusetariff/groupe/TimeOfUseTariffGroupeImplTest.java @@ -10,7 +10,6 @@ import org.junit.Test; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.oem.DummyOpenemsEdgeOem; import io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; @@ -410,11 +409,9 @@ public void test() throws Exception { new ComponentTest(groupe) // .addReference("httpBridgeFactory", DummyBridgeHttpFactory.ofDummyBridge()) // .addReference("meta", dummyMeta) // - .addReference("oem", new DummyOpenemsEdgeOem()) // .addReference("componentManager", new DummyComponentManager(clock)) // .activate(MyConfig.create() // .setId("ctrl0") // - .setExchangerateAccesskey("") // .build()) // ; } diff --git a/io.openems.edge.timeofusetariff.swisspower/.classpath b/io.openems.edge.timeofusetariff.swisspower/.classpath new file mode 100644 index 00000000000..bbfbdbe40e7 --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/io.openems.edge.timeofusetariff.swisspower/.gitignore b/io.openems.edge.timeofusetariff.swisspower/.gitignore new file mode 100644 index 00000000000..c2b941a96de --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/.gitignore @@ -0,0 +1,2 @@ +/bin_test/ +/generated/ diff --git a/io.openems.edge.timeofusetariff.swisspower/.project b/io.openems.edge.timeofusetariff.swisspower/.project new file mode 100644 index 00000000000..f72a46f1fa2 --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/.project @@ -0,0 +1,23 @@ + + + io.openems.edge.timeofusetariff.swisspower + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/io.openems.edge.timeofusetariff.swisspower/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.timeofusetariff.swisspower/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/io.openems.edge.timeofusetariff.swisspower/bnd.bnd b/io.openems.edge.timeofusetariff.swisspower/bnd.bnd new file mode 100644 index 00000000000..5c1ca041564 --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/bnd.bnd @@ -0,0 +1,14 @@ +Bundle-Name: OpenEMS Edge Time-Of-Use Tariff Swisspower +Bundle-Vendor: FENECON GmbH +Bundle-License: https://opensource.org/licenses/EPL-2.0 +Bundle-Version: 1.0.0.${tstamp} + +-buildpath: \ + ${buildpath},\ + io.openems.common,\ + io.openems.edge.bridge.http,\ + io.openems.edge.common,\ + io.openems.edge.timeofusetariff.api,\ + +-testpath: \ + ${testpath} \ No newline at end of file diff --git a/io.openems.edge.timeofusetariff.swisspower/readme.adoc b/io.openems.edge.timeofusetariff.swisspower/readme.adoc new file mode 100644 index 00000000000..c470cec24ab --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/readme.adoc @@ -0,0 +1,11 @@ += Time-Of-Use Tariff Swisspower + +Retrieves the quarterly prices from the Swisspower ESIT API. + +For detailed information about the Swisspower ESIT API, please refer to: https://esit-test.code-fabrik.ch/doc_scalar/ + +Prices retrieved from Swisspower are subsequently converted to the user's currency (defined in Core.Meta Component) using the Exchange Rates API. + +For detailed information about the Exchange Rates API, please refer to: https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html + +https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.timeofusetariff.swisspower[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/Config.java b/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/Config.java new file mode 100644 index 00000000000..bb805a5543f --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/Config.java @@ -0,0 +1,28 @@ +package io.openems.edge.timeofusetariff.swisspower; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.AttributeType; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(// + name = "Time-Of-Use Tariff Swisspower", // + description = "Time-Of-Use Tariff implementation for Swisspower.") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "timeOfUseTariff0"; + + @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") + String alias() default ""; + + @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") + boolean enabled() default true; + + @AttributeDefinition(name = "Access Token", description = "Access token for the Swisspower Platform", type = AttributeType.PASSWORD) + String accessToken() default ""; + + @AttributeDefinition(name = "Measuring point number", description = "Measuring point number for which the tariff is to be retrieved. If this option is used, the tariff name is automatically selected.") + String meteringCode() default ""; + + String webconsole_configurationFactory_nameHint() default "Time-Of-Use Tariff Swisspower [{id}]"; +} diff --git a/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspower.java b/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspower.java new file mode 100644 index 00000000000..f718d966da5 --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspower.java @@ -0,0 +1,37 @@ +package io.openems.edge.timeofusetariff.swisspower; + +import io.openems.common.channel.Level; +import io.openems.common.types.OpenemsType; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; + +public interface TimeOfUseTariffSwisspower extends TimeOfUseTariff, OpenemsComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + HTTP_STATUS_CODE(Doc.of(OpenemsType.INTEGER)// + .text("Displays the HTTP status code")), // + STATUS_INVALID_FIELDS(Doc.of(Level.WARNING) // + .text("Unable to update prices: please check your access token and metering code")), // + /** + * Should never happen. Only happens if the request has missing fields or wrong + * format of timestamps. + */ + STATUS_BAD_REQUEST(Doc.of(Level.FAULT) // + .text("Unable to update prices: internal error")), // + STATUS_READ_TIMEOUT(Doc.of(Level.WARNING) // + .text("Unable to update prices: read timeout error")), // + ; + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } +} diff --git a/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspowerImpl.java b/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspowerImpl.java new file mode 100644 index 00000000000..920c74bf7f3 --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/src/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspowerImpl.java @@ -0,0 +1,272 @@ +package io.openems.edge.timeofusetariff.swisspower; + +import static io.openems.common.utils.JsonUtils.getAsDouble; +import static io.openems.common.utils.JsonUtils.getAsJsonArray; +import static io.openems.common.utils.JsonUtils.getAsString; +import static io.openems.common.utils.JsonUtils.parseToJsonObject; +import static io.openems.edge.timeofusetariff.api.utils.ExchangeRateApi.getExchangeRateOrElse; +import static io.openems.edge.timeofusetariff.api.utils.TimeOfUseTariffUtils.generateDebugLog; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.time.Clock; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; + +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.timedata.DurationUnit; +import io.openems.edge.bridge.http.api.BridgeHttp; +import io.openems.edge.bridge.http.api.BridgeHttp.Endpoint; +import io.openems.edge.bridge.http.api.BridgeHttpFactory; +import io.openems.edge.bridge.http.api.HttpError; +import io.openems.edge.bridge.http.api.HttpMethod; +import io.openems.edge.bridge.http.api.HttpResponse; +import io.openems.edge.bridge.http.api.UrlBuilder; +import io.openems.edge.bridge.http.time.DelayTimeProvider; +import io.openems.edge.bridge.http.time.DelayTimeProviderChain; +import io.openems.edge.common.channel.value.Value; +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.currency.Currency; +import io.openems.edge.common.meta.Meta; +import io.openems.edge.timeofusetariff.api.TimeOfUsePrices; +import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "TimeOfUseTariff.Swisspower", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class TimeOfUseTariffSwisspowerImpl extends AbstractOpenemsComponent + implements TimeOfUseTariff, OpenemsComponent, TimeOfUseTariffSwisspower { + private static final UrlBuilder URL_BASE = UrlBuilder.parse("https://esit.code-fabrik.ch/api/v1/metering_code"); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + private static final int INTERNAL_ERROR = -1; // parsing, handle exception... + private static final int DEFAULT_READ_TIMEOUT = 200; + protected static final int SERVER_ERROR_CODE = 500; + protected static final int BAD_REQUEST_ERROR_CODE = 400; + + private final Logger log = LoggerFactory.getLogger(TimeOfUseTariffSwisspowerImpl.class); + private final AtomicReference prices = new AtomicReference<>(TimeOfUsePrices.EMPTY_PRICES); + private String accessToken = null; + private String meteringCode = null; + + @Reference + private BridgeHttpFactory httpBridgeFactory; + private BridgeHttp httpBridge; + + @Reference + private Meta meta; + + @Reference + private ComponentManager componentManager; + + public TimeOfUseTariffSwisspowerImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + TimeOfUseTariffSwisspower.ChannelId.values() // + ); + } + + private final BiConsumer, Value> onCurrencyChange = (a, b) -> { + this.httpBridge.removeAllTimeEndpoints(); + this.httpBridge.subscribeTime(new SwisspowerProvider(this.componentManager.getClock()), // + this::createSwisspowerEndpoint, // + this::handleEndpointResponse, // + this::handleEndpointError); + }; + + @Activate + private void activate(ComponentContext context, Config config) { + super.activate(context, config.id(), config.alias(), config.enabled()); + + if (!config.enabled()) { + return; + } + + this.accessToken = config.accessToken(); + if (this.accessToken == null) { + this.logError(this.log, "Please configure personal Access token to access Swisspower API"); + return; + } + + this.meteringCode = config.meteringCode(); + if (this.meteringCode == null) { + this.logError(this.log, "Please configure meteringCode to access Swisspower API"); + return; + } + + // React on updates to Currency. + this.meta.getCurrencyChannel().onChange(this.onCurrencyChange); + + this.httpBridge = this.httpBridgeFactory.get(); + this.httpBridge.subscribeTime(new SwisspowerProvider(this.componentManager.getClock()), // + this::createSwisspowerEndpoint, // + this::handleEndpointResponse, // + this::handleEndpointError); + } + + private Endpoint createSwisspowerEndpoint() { + final var now = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS); + final var startTimestamp = now.format(DATE_FORMATTER); // eg. 2024-05-22T00:00:00+02:00 + final var endTimestamp = now.plusDays(1).format(DATE_FORMATTER); + + final var url = URL_BASE.withQueryParam("start_timestamp", startTimestamp) // + .withQueryParam("end_timestamp", endTimestamp) // + .withQueryParam("metering_code", this.meteringCode); + + return new Endpoint(url.toEncodedString(), // + HttpMethod.GET, // + BridgeHttp.DEFAULT_CONNECT_TIMEOUT, // + DEFAULT_READ_TIMEOUT, // + null, // + this.buildRequestHeaders()); + } + + private Map buildRequestHeaders() { + return Map.of(// + "Authorization", "Bearer " + this.accessToken // + ); + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + this.meta.getCurrencyChannel().removeOnChangeCallback(this.onCurrencyChange); + this.httpBridgeFactory.unget(this.httpBridge); + } + + public static class SwisspowerProvider implements DelayTimeProvider { + + private final Clock clock; + + public SwisspowerProvider(Clock clock) { + super(); + this.clock = clock; + } + + @Override + public Delay onFirstRunDelay() { + return Delay.of(Duration.ofMinutes(1)); + } + + @Override + public Delay onErrorRunDelay(HttpError error) { + return DelayTimeProviderChain.fixedDelay(Duration.ofHours(1))// + .plusRandomDelay(60, ChronoUnit.SECONDS) // + .getDelay(); + } + + @Override + public Delay onSuccessRunDelay(HttpResponse result) { + return DelayTimeProviderChain.fixedAtEveryFull(this.clock, DurationUnit.ofDays(1)) + .plusRandomDelay(60, ChronoUnit.SECONDS) // + .getDelay(); + } + } + + private void handleEndpointResponse(HttpResponse response) throws OpenemsNamedException, IOException { + this.setChannelValues(response.status().code(), false, false, false); + + final var swissPowerCurrency = Currency.CHF.name(); // Swiss Franc + final var globalCurrency = this.meta.getCurrency(); + final double exchangeRate = getExchangeRateOrElse(swissPowerCurrency, globalCurrency, 1.); + + // Parse the response for the prices + this.prices.set(parsePrices(response.data(), exchangeRate)); + } + + private void handleEndpointError(HttpError error) { + var httpStatusCode = (error instanceof HttpError.ResponseError re) ? re.status.code() : INTERNAL_ERROR; + var serverError = (httpStatusCode == SERVER_ERROR_CODE); + var badRequest = (httpStatusCode == BAD_REQUEST_ERROR_CODE); + var timeoutError = (error instanceof HttpError.UnknownError e + && e.getCause() instanceof SocketTimeoutException); + + this.setChannelValues(httpStatusCode, serverError, badRequest, timeoutError); + this.log.error("HTTP Error [{}]: {}", httpStatusCode, error.getMessage()); + } + + @Override + public TimeOfUsePrices getPrices() { + return TimeOfUsePrices.from(ZonedDateTime.now(this.componentManager.getClock()), this.prices.get()); + } + + /** + * Parses JSON data to extract time-of-use prices and returns a + * {@link TimeOfUsePrices} object. + * + * @param jsonData the JSON data as a {@code String} containing the + * electricity price information. + * @param exchangeRate The exchange rate of user currency to EUR. + * @return a {@link TimeOfUsePrices} object containing the parsed prices mapped + * to their respective timestamps. + * @throws OpenemsNamedException if an error occurs during the parsing of the + * JSON data. + */ + protected static TimeOfUsePrices parsePrices(String jsonData, double exchangeRate) throws OpenemsNamedException { + var result = new TreeMap(); + var data = parseToJsonObject(jsonData); + var prices = getAsJsonArray(data, "prices"); + + for (var element : prices) { + + var startTimeString = getAsString(element, "start_timestamp"); + var integrated = getAsJsonArray(element, "integrated"); + + // CHF/kWh -> Currency/MWh + // Example: 0.1 CHF/kWh * 1000 = 100 CHF/MWh. + var marketPrice = getAsDouble(integrated.get(0), "value") * 1000 * exchangeRate; + + // Convert LocalDateTime to ZonedDateTime + var startTimeStamp = ZonedDateTime.parse(startTimeString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + + // Adding the values in the Map. + result.put(startTimeStamp, marketPrice); + } + return TimeOfUsePrices.from(result); + } + + /** + * This method updates the channel values in response to an HTTP request. + * + * @param httpStatusCode the HTTP status code returned from the endpoint + * response + * @param serverError a boolean indicating if a server error occurred (status + * code 500) + * @param badRequest a boolean indicating if the request was invalid (status + * code 400) + * @param timeoutError a boolean indicating if the request could not be read + * in time + */ + private void setChannelValues(int httpStatusCode, boolean serverError, boolean badRequest, boolean timeoutError) { + this.channel(TimeOfUseTariffSwisspower.ChannelId.HTTP_STATUS_CODE).setNextValue(httpStatusCode); + this.channel(TimeOfUseTariffSwisspower.ChannelId.STATUS_INVALID_FIELDS).setNextValue(serverError); + this.channel(TimeOfUseTariffSwisspower.ChannelId.STATUS_BAD_REQUEST).setNextValue(badRequest); + this.channel(TimeOfUseTariffSwisspower.ChannelId.STATUS_READ_TIMEOUT).setNextValue(timeoutError); + } + + @Override + public String debugLog() { + return generateDebugLog(this, this.meta.getCurrency()); + } +} diff --git a/io.openems.edge.timeofusetariff.swisspower/test/.gitignore b/io.openems.edge.timeofusetariff.swisspower/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/io.openems.edge.timeofusetariff.swisspower/test/io/openems/edge/timeofusetariff/swisspower/MyConfig.java b/io.openems.edge.timeofusetariff.swisspower/test/io/openems/edge/timeofusetariff/swisspower/MyConfig.java new file mode 100644 index 00000000000..ea1be73e72e --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/test/io/openems/edge/timeofusetariff/swisspower/MyConfig.java @@ -0,0 +1,62 @@ +package io.openems.edge.timeofusetariff.swisspower; + +import io.openems.common.test.AbstractComponentConfig; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + public static class Builder { + private String id; + private String accessToken; + private String meteringCode; + + private Builder() { + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setAccessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + public Builder setMeteringCode(String meteringCode) { + this.meteringCode = meteringCode; + return this; + } + + public MyConfig build() { + return new MyConfig(this); + } + } + + /** + * Create a Config builder. + * + * @return a {@link Builder} + */ + public static Builder create() { + return new Builder(); + } + + private final Builder builder; + + private MyConfig(Builder builder) { + super(Config.class, builder.id); + this.builder = builder; + } + + @Override + public String accessToken() { + return this.builder.accessToken; + } + + @Override + public String meteringCode() { + return this.builder.meteringCode; + } + +} \ No newline at end of file diff --git a/io.openems.edge.timeofusetariff.swisspower/test/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspowerImplTest.java b/io.openems.edge.timeofusetariff.swisspower/test/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspowerImplTest.java new file mode 100644 index 00000000000..da9f93d852b --- /dev/null +++ b/io.openems.edge.timeofusetariff.swisspower/test/io/openems/edge/timeofusetariff/swisspower/TimeOfUseTariffSwisspowerImplTest.java @@ -0,0 +1,226 @@ +package io.openems.edge.timeofusetariff.swisspower; + +import static io.openems.edge.common.currency.Currency.EUR; +import static io.openems.edge.common.test.TestUtils.createDummyClock; +import static io.openems.edge.timeofusetariff.swisspower.TimeOfUseTariffSwisspowerImpl.parsePrices; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import org.junit.Test; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory; +import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.DummyMeta; + +public class TimeOfUseTariffSwisspowerImplTest { + + private static final String CTRL_ID = "ctrl0"; + private static final double GROUPE_E_EXCHANGE_RATE = 1; + + private static final String PRICE_RESULT_STRING = """ + { + "status": "ok", + "prices": [ + { + "start_timestamp": "2024-08-12T00:00:00+02:00", + "end_timestamp": "2024-08-12T00:15:00+02:00", + "integrated": [ + { + "value": 0.49249999999999994, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T00:15:00+02:00", + "end_timestamp": "2024-08-12T00:30:00+02:00", + "integrated": [ + { + "value": 0.491133, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T00:30:00+02:00", + "end_timestamp": "2024-08-12T00:45:00+02:00", + "integrated": [ + { + "value": 0.486722, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T00:45:00+02:00", + "end_timestamp": "2024-08-12T01:00:00+02:00", + "integrated": [ + { + "value": 0.478854, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T01:00:00+02:00", + "end_timestamp": "2024-08-12T01:15:00+02:00", + "integrated": [ + { + "value": 0.46720300000000003, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T01:15:00+02:00", + "end_timestamp": "2024-08-12T01:30:00+02:00", + "integrated": [ + { + "value": 0.451539, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T01:30:00+02:00", + "end_timestamp": "2024-08-12T01:45:00+02:00", + "integrated": [ + { + "value": 0.431753, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T01:45:00+02:00", + "end_timestamp": "2024-08-12T02:00:00+02:00", + "integrated": [ + { + "value": 0.407858, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T02:00:00+02:00", + "end_timestamp": "2024-08-12T02:15:00+02:00", + "integrated": [ + { + "value": 0.38, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T02:15:00+02:00", + "end_timestamp": "2024-08-12T02:30:00+02:00", + "integrated": [ + { + "value": 0.348458, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T02:30:00+02:00", + "end_timestamp": "2024-08-12T02:45:00+02:00", + "integrated": [ + { + "value": 0.3425, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T02:45:00+02:00", + "end_timestamp": "2024-08-12T03:00:00+02:00", + "integrated": [ + { + "value": 0.3425, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T03:00:00+02:00", + "end_timestamp": "2024-08-12T03:15:00+02:00", + "integrated": [ + { + "value": 0.3425, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T03:15:00+02:00", + "end_timestamp": "2024-08-12T03:30:00+02:00", + "integrated": [ + { + "value": 0.3425, + "unit": "CHF/kWh", + "component": "work" + } + ] + }, + { + "start_timestamp": "2024-08-12T03:30:00+02:00", + "end_timestamp": "2024-08-12T03:45:00+02:00", + "integrated": [ + { + "value": 0.3425, + "unit": "CHF/kWh", + "component": "work" + } + ] + } + ] + } + + """; + + @Test + public void test() throws Exception { + final var clock = createDummyClock(); + var swissPower = new TimeOfUseTariffSwisspowerImpl(); + var dummyMeta = new DummyMeta("foo0") // + .withCurrency(EUR); + new ComponentTest(swissPower) // + .addReference("httpBridgeFactory", DummyBridgeHttpFactory.ofDummyBridge()) // + .addReference("meta", dummyMeta) // + .addReference("componentManager", new DummyComponentManager(clock)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setAccessToken("foo-bar") // + .setMeteringCode("") // + .build()) // + ; + } + + @Test + public void nonEmptyStringTest() throws OpenemsNamedException { + // Parsing with custom data + var prices = parsePrices(PRICE_RESULT_STRING, GROUPE_E_EXCHANGE_RATE); // + + // To check if the Map is not empty + assertFalse(prices.isEmpty()); + + // To check if a value is present in map. + assertEquals(492.499, prices.getFirst(), 0.001); + } + +} diff --git a/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/chart/chart.ts b/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/chart/chart.ts index 88a6d60c838..4ed2a8d0982 100644 --- a/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/chart/chart.ts +++ b/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/chart/chart.ts @@ -49,7 +49,7 @@ export class GridOptimizedChargeChartComponent extends AbstractHistoryChart { borderDash: [3, 3], }, { - name: translate.instant("General.chargePower"), + name: translate.instant("General.CHARGE"), converter: () => (data["ProductionDcActualPower"] ? diff --git a/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/overview/overview.html b/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/overview/overview.html index 0c7ce394dd6..1a8f64c0c8b 100644 --- a/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/overview/overview.html +++ b/ui/src/app/edge/history/Controller/Ess/GridoptimizedCharge/overview/overview.html @@ -1,6 +1,6 @@ - + diff --git a/ui/src/app/edge/history/Controller/Ess/TimeOfUseTariff/chart/chart.ts b/ui/src/app/edge/history/Controller/Ess/TimeOfUseTariff/chart/chart.ts index 6a7850fcafb..b0df0e20a67 100644 --- a/ui/src/app/edge/history/Controller/Ess/TimeOfUseTariff/chart/chart.ts +++ b/ui/src/app/edge/history/Controller/Ess/TimeOfUseTariff/chart/chart.ts @@ -3,6 +3,7 @@ import { Component, Input } from "@angular/core"; import * as Chart from "chart.js"; import { calculateResolution, ChronoUnit, Resolution } from "src/app/edge/history/shared"; import { AbstractHistoryChart } from "src/app/shared/components/chart/abstracthistorychart"; +import { ChartConstants } from "src/app/shared/components/chart/chart.constants"; import { ChartAxis, HistoryUtils, TimeOfUseTariffUtils, Utils, YAxisType } from "src/app/shared/service/utils"; import { ChannelAddress, Currency, EdgeConfig } from "src/app/shared/shared"; import { ColorUtils } from "src/app/shared/utils/color/color.utils"; @@ -165,7 +166,10 @@ export class ChartComponent extends AbstractHistoryChart { el.borderColor = ColorUtils.changeOpacityFromRGBA(el.borderColor.toString(), 1); return el; }); - + this.options.scales[ChartAxis.LEFT].ticks = { + ...this.options.scales[ChartAxis.LEFT].ticks, + ...ChartConstants.DEFAULT_Y_SCALE_OPTIONS(this.chartObject.yAxes.find(el => el.unit === YAxisType.CURRENCY), this.translate, "line", this.datasets, true).ticks, + }; this.options.scales.x["offset"] = false; this.options["animation"] = false; }); diff --git a/ui/src/app/edge/history/abstracthistorychart.ts b/ui/src/app/edge/history/abstracthistorychart.ts index 7c8c70ec7bb..84a3923f138 100644 --- a/ui/src/app/edge/history/abstracthistorychart.ts +++ b/ui/src/app/edge/history/abstracthistorychart.ts @@ -2,7 +2,7 @@ import { TranslateService } from "@ngx-translate/core"; import * as Chart from "chart.js"; import { AbstractHistoryChart as NewAbstractHistoryChart } from "src/app/shared/components/chart/abstracthistorychart"; -import { ChartConstants, XAxisType } from "src/app/shared/components/chart/chart.constants"; +import { XAxisType } from "src/app/shared/components/chart/chart.constants"; import { JsonrpcResponseError } from "src/app/shared/jsonrpc/base"; import { QueryHistoricTimeseriesDataRequest } from "src/app/shared/jsonrpc/request/queryHistoricTimeseriesDataRequest"; import { QueryHistoricTimeseriesEnergyPerPeriodRequest } from "src/app/shared/jsonrpc/request/queryHistoricTimeseriesEnergyPerPeriodRequest"; @@ -234,19 +234,18 @@ export abstract class AbstractHistoryChart { break; } - // Only one yAxis defined - options = NewAbstractHistoryChart.getYAxisOptions(options, yAxis, this.translate, "line", locale, ChartConstants.EMPTY_DATASETS, false); - - options.scales.x["stacked"] = true; - options.scales[ChartAxis.LEFT]["stacked"] = false; - options = NewAbstractHistoryChart.applyChartTypeSpecificOptionsChanges("line", options, this.service, chartObject); - /** Overwrite default yAxisId */ this.datasets = this.datasets .map(el => { el["yAxisID"] = ChartAxis.LEFT; return el; }); + + // Only one yAxis defined + options = NewAbstractHistoryChart.getYAxisOptions(options, yAxis, this.translate, "line", locale, this.datasets, true); + options = NewAbstractHistoryChart.applyChartTypeSpecificOptionsChanges("line", options, this.service, chartObject); + options.scales[ChartAxis.LEFT]["stacked"] = false; + options.scales.x["stacked"] = true; }).then(() => { this.options = options; resolve(); @@ -345,8 +344,7 @@ export abstract class AbstractHistoryChart { * @returns the ChartOptions */ protected createDefaultChartOptions(): Chart.ChartOptions { - const options = Utils.deepCopy(DEFAULT_TIME_CHART_OPTIONS); - return options; + return Utils.deepCopy(DEFAULT_TIME_CHART_OPTIONS); } /** diff --git a/ui/src/app/edge/history/common/energy/chart/chart.ts b/ui/src/app/edge/history/common/energy/chart/chart.ts index 9598a1615a0..13f6ea38376 100644 --- a/ui/src/app/edge/history/common/energy/chart/chart.ts +++ b/ui/src/app/edge/history/common/energy/chart/chart.ts @@ -101,7 +101,7 @@ export class ChartComponent extends AbstractHistoryChart { // Charge Power { - name: translate.instant("General.chargePower"), + name: translate.instant("General.CHARGE"), nameSuffix: (energyValues: QueryHistoricTimeseriesEnergyResponse) => energyValues.result.data["_sum/EssDcChargeEnergy"], converter: () => chartType === "line" // ? data["EssCharge"]?.map((value, index) => { @@ -114,7 +114,7 @@ export class ChartComponent extends AbstractHistoryChart { // Discharge Power { - name: translate.instant("General.dischargePower"), + name: translate.instant("General.DISCHARGE"), nameSuffix: (energyValues: QueryHistoricTimeseriesEnergyResponse) => energyValues.result.data["_sum/EssDcDischargeEnergy"], converter: () => { return chartType === "line" ? diff --git a/ui/src/app/edge/history/delayedselltogrid/chart.component.ts b/ui/src/app/edge/history/delayedselltogrid/chart.component.ts index deb3efeec3c..00a29929efc 100644 --- a/ui/src/app/edge/history/delayedselltogrid/chart.component.ts +++ b/ui/src/app/edge/history/delayedselltogrid/chart.component.ts @@ -147,7 +147,7 @@ export class DelayedSellToGridChartComponent extends AbstractHistoryChart implem } }); datasets.push({ - label: this.translate.instant("General.chargePower"), + label: this.translate.instant("General.CHARGE"), data: chargeData, borderDash: [10, 10], }); @@ -168,7 +168,7 @@ export class DelayedSellToGridChartComponent extends AbstractHistoryChart implem } }); datasets.push({ - label: this.translate.instant("General.dischargePower"), + label: this.translate.instant("General.DISCHARGE"), data: dischargeData, borderDash: [10, 10], }); diff --git a/ui/src/app/edge/history/peakshaving/asymmetric/chart.component.ts b/ui/src/app/edge/history/peakshaving/asymmetric/chart.component.ts index d9b4b4ae008..aeb02eda9e6 100644 --- a/ui/src/app/edge/history/peakshaving/asymmetric/chart.component.ts +++ b/ui/src/app/edge/history/peakshaving/asymmetric/chart.component.ts @@ -178,7 +178,7 @@ export class AsymmetricPeakshavingChartComponent extends AbstractHistoryChart im } }); datasets.push({ - label: this.translate.instant("General.chargePower"), + label: this.translate.instant("General.CHARGE"), data: chargeData, }); this.colors.push({ @@ -198,7 +198,7 @@ export class AsymmetricPeakshavingChartComponent extends AbstractHistoryChart im } }); datasets.push({ - label: this.translate.instant("General.dischargePower"), + label: this.translate.instant("General.DISCHARGE"), data: dischargeData, }); this.colors.push({ diff --git a/ui/src/app/edge/history/peakshaving/symmetric/chart.component.ts b/ui/src/app/edge/history/peakshaving/symmetric/chart.component.ts index 4c9d97e725f..25aa0c7cc0b 100644 --- a/ui/src/app/edge/history/peakshaving/symmetric/chart.component.ts +++ b/ui/src/app/edge/history/peakshaving/symmetric/chart.component.ts @@ -145,7 +145,7 @@ export class SymmetricPeakshavingChartComponent extends AbstractHistoryChart imp } }); datasets.push({ - label: this.translate.instant("General.chargePower"), + label: this.translate.instant("General.CHARGE"), data: chargeData, borderDash: [10, 10], }); @@ -166,7 +166,7 @@ export class SymmetricPeakshavingChartComponent extends AbstractHistoryChart imp } }); datasets.push({ - label: this.translate.instant("General.dischargePower"), + label: this.translate.instant("General.DISCHARGE"), data: dischargeData, borderDash: [10, 10], }); diff --git a/ui/src/app/edge/history/peakshaving/timeslot/chart.component.ts b/ui/src/app/edge/history/peakshaving/timeslot/chart.component.ts index 4a62f084032..f54aad37448 100644 --- a/ui/src/app/edge/history/peakshaving/timeslot/chart.component.ts +++ b/ui/src/app/edge/history/peakshaving/timeslot/chart.component.ts @@ -159,7 +159,7 @@ export class TimeslotPeakshavingChartComponent extends AbstractHistoryChart impl } }); datasets.push({ - label: this.translate.instant("General.chargePower"), + label: this.translate.instant("General.CHARGE"), data: chargeData, borderDash: [10, 10], }); @@ -180,7 +180,7 @@ export class TimeslotPeakshavingChartComponent extends AbstractHistoryChart impl } }); datasets.push({ - label: this.translate.instant("General.dischargePower"), + label: this.translate.instant("General.DISCHARGE"), data: dischargeData, borderDash: [10, 10], }); diff --git a/ui/src/app/edge/history/storage/chargerchart.component.ts b/ui/src/app/edge/history/storage/chargerchart.component.ts index 44395de3842..d13d435b02f 100644 --- a/ui/src/app/edge/history/storage/chargerchart.component.ts +++ b/ui/src/app/edge/history/storage/chargerchart.component.ts @@ -70,7 +70,7 @@ export class StorageChargerChartComponent extends AbstractHistoryChart implement }); if (address.channelId == "ActualPower") { datasets.push({ - label: this.translate.instant("General.chargePower"), + label: this.translate.instant("General.CHARGE"), data: chargerData, hidden: false, }); diff --git a/ui/src/app/edge/history/storage/singlechart.component.ts b/ui/src/app/edge/history/storage/singlechart.component.ts index 4d987a4b2ee..7bd11ca1a41 100644 --- a/ui/src/app/edge/history/storage/singlechart.component.ts +++ b/ui/src/app/edge/history/storage/singlechart.component.ts @@ -216,15 +216,15 @@ export class StorageSingleChartComponent extends AbstractHistoryChart implements // 0.005 to prevent showing Charge or Discharge if value is e.g. 0.00232138 if (value < -0.005) { if (label.includes(translate.instant("General.phase"))) { - label += " " + translate.instant("General.chargePower"); + label += " " + translate.instant("General.CHARGE"); } else { - label = translate.instant("General.chargePower"); + label = translate.instant("General.CHARGE"); } } else if (value > 0.005) { if (label.includes(translate.instant("General.phase"))) { - label += " " + translate.instant("General.dischargePower"); + label += " " + translate.instant("General.DISCHARGE"); } else { - label = translate.instant("General.dischargePower"); + label = translate.instant("General.DISCHARGE"); } } return label + ": " + formatNumber(value, "de", "1.0-2") + " kW"; diff --git a/ui/src/app/edge/history/storage/totalchart.component.ts b/ui/src/app/edge/history/storage/totalchart.component.ts index c774c6139ef..fb73aec189d 100644 --- a/ui/src/app/edge/history/storage/totalchart.component.ts +++ b/ui/src/app/edge/history/storage/totalchart.component.ts @@ -252,9 +252,9 @@ export class StorageTotalChartComponent extends AbstractHistoryChart implements const value = tooltipItem.dataset.data[tooltipItem.dataIndex]; // 0.005 to prevent showing Charge or Discharge if value is e.g. 0.00232138 if (value < -0.005) { - label += " " + translate.instant("General.chargePower"); + label += " " + translate.instant("General.CHARGE"); } else if (value > 0.005) { - label += " " + translate.instant("General.dischargePower"); + label += " " + translate.instant("General.DISCHARGE"); } return label + ": " + formatNumber(value, "de", "1.0-2") + " kW"; }; diff --git a/ui/src/app/edge/history/storage/widget.component.html b/ui/src/app/edge/history/storage/widget.component.html index 79f3aaac4ce..6fc91033a0e 100644 --- a/ui/src/app/edge/history/storage/widget.component.html +++ b/ui/src/app/edge/history/storage/widget.component.html @@ -7,13 +7,13 @@ - + - + diff --git a/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/powerSocChart.ts b/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/powerSocChart.ts index 61e457c6d86..ed22926be64 100644 --- a/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/powerSocChart.ts +++ b/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/powerSocChart.ts @@ -146,7 +146,7 @@ export class SchedulePowerAndSocChartComponent extends AbstractHistoryChart impl datasets.push({ type: "line", - label: this.translate.instant("General.chargePower"), + label: this.translate.instant("General.CHARGE"), data: essChargeArray.map(v => Utils.divideSafely(v, 1000)), // [W] to [kW] hidden: true, order: 1, @@ -159,7 +159,7 @@ export class SchedulePowerAndSocChartComponent extends AbstractHistoryChart impl datasets.push({ type: "line", - label: this.translate.instant("General.dischargePower"), + label: this.translate.instant("General.DISCHARGE"), data: essDischargeArray.map(v => Utils.divideSafely(v, 1000)), // [W] to [kW] hidden: true, order: 1, diff --git a/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/statePriceChart.ts b/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/statePriceChart.ts index 63aaa9ac05b..6061d016c14 100644 --- a/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/statePriceChart.ts +++ b/ui/src/app/edge/live/Controller/Ess/TimeOfUseTariff/modal/statePriceChart.ts @@ -107,11 +107,11 @@ export class ScheduleStateAndPriceChartComponent extends AbstractHistoryChart im private applyControllerSpecificOptions() { const locale = this.service.translate.currentLang; - const rightYaxisSoc: HistoryUtils.yAxes = { position: "right", unit: YAxisType.PERCENTAGE, yAxisId: ChartAxis.RIGHT }; - this.options = NewAbstractHistoryChart.getYAxisOptions(this.options, rightYaxisSoc, this.translate, "line", locale, ChartConstants.EMPTY_DATASETS); + const rightYaxisSoc: HistoryUtils.yAxes = { position: "right", unit: YAxisType.PERCENTAGE, yAxisId: ChartAxis.RIGHT, displayGrid: true }; + this.options = NewAbstractHistoryChart.getYAxisOptions(this.options, rightYaxisSoc, this.translate, "line", locale, this.datasets, true); const rightYAxisPower: HistoryUtils.yAxes = { position: "right", unit: YAxisType.POWER, yAxisId: ChartAxis.RIGHT_2 }; - this.options = NewAbstractHistoryChart.getYAxisOptions(this.options, rightYAxisPower, this.translate, "line", locale, ChartConstants.EMPTY_DATASETS); + this.options = NewAbstractHistoryChart.getYAxisOptions(this.options, rightYAxisPower, this.translate, "line", locale, this.datasets, true); this.options.scales.x["time"].unit = calculateResolution(this.service, this.service.historyPeriod.value.from, this.service.historyPeriod.value.to).timeFormat; this.options.scales.x["ticks"] = { source: "auto", autoSkip: false }; @@ -165,13 +165,19 @@ export class ScheduleStateAndPriceChartComponent extends AbstractHistoryChart im return el; }); + const leftYAxis: HistoryUtils.yAxes = { position: "left", unit: this.unit, yAxisId: ChartAxis.LEFT, customTitle: this.currencyLabel }; + [rightYaxisSoc, rightYAxisPower].forEach((element) => { + this.options = NewAbstractHistoryChart.getYAxisOptions(this.options, element, this.translate, "line", locale, this.datasets, true); + }); - this.options.scales[ChartAxis.LEFT]["title"].text = this.currencyLabel; + this.options.scales[ChartAxis.LEFT] = { + ...this.options.scales[ChartAxis.LEFT], + ...ChartConstants.DEFAULT_Y_SCALE_OPTIONS(leftYAxis, this.translate, "line", this.datasets.filter(el => el["yAxisID"] === ChartAxis.LEFT), true), + }; this.options.scales[ChartAxis.RIGHT].grid.display = false; this.options.scales[ChartAxis.RIGHT_2].suggestedMin = 0; this.options.scales[ChartAxis.RIGHT_2].suggestedMax = 1; this.options.scales[ChartAxis.RIGHT_2].grid.display = false; this.options["animation"] = false; } - } diff --git a/ui/src/app/edge/live/common/storage/modal/modal.component.html b/ui/src/app/edge/live/common/storage/modal/modal.component.html index 4e83cd402e7..1a69b3ebe9b 100644 --- a/ui/src/app/edge/live/common/storage/modal/modal.component.html +++ b/ui/src/app/edge/live/common/storage/modal/modal.component.html @@ -30,12 +30,12 @@ - + - + @@ -52,7 +52,7 @@ - + - + @@ -340,7 +340,7 @@ - + - + - + - + diff --git a/ui/src/app/edge/live/common/storage/storage.component.html b/ui/src/app/edge/live/common/storage/storage.component.html index 6f8c5db2bb1..47d39d8842e 100644 --- a/ui/src/app/edge/live/common/storage/storage.component.html +++ b/ui/src/app/edge/live/common/storage/storage.component.html @@ -30,20 +30,20 @@ - + - - - diff --git a/ui/src/app/edge/settings/app/index.component.html b/ui/src/app/edge/settings/app/index.component.html index fd010aeac6a..b7225ae5673 100644 --- a/ui/src/app/edge/settings/app/index.component.html +++ b/ui/src/app/edge/settings/app/index.component.html @@ -3,7 +3,6 @@ - {{ app.shortName ?? app.name }} diff --git a/ui/src/app/edge/settings/app/install.component.html b/ui/src/app/edge/settings/app/install.component.html index b9fe99c5215..9b805f1fd96 100644 --- a/ui/src/app/edge/settings/app/install.component.html +++ b/ui/src/app/edge/settings/app/install.component.html @@ -7,7 +7,6 @@
- {{ appName }} diff --git a/ui/src/app/edge/settings/app/update.component.html b/ui/src/app/edge/settings/app/update.component.html index e336e7124de..dff8a08141c 100644 --- a/ui/src/app/edge/settings/app/update.component.html +++ b/ui/src/app/edge/settings/app/update.component.html @@ -5,7 +5,6 @@ - {{ appName }} diff --git a/ui/src/app/edge/settings/system/system.component.ts b/ui/src/app/edge/settings/system/system.component.ts index e6398f41b90..4040aeea1a7 100644 --- a/ui/src/app/edge/settings/system/system.component.ts +++ b/ui/src/app/edge/settings/system/system.component.ts @@ -1,6 +1,5 @@ // @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; +import { Component, effect } from "@angular/core"; import { environment } from "src/environments"; import { Edge, Service, UserPermission, Utils } from "../../../shared/shared"; @@ -8,30 +7,26 @@ import { Edge, Service, UserPermission, Utils } from "../../../shared/shared"; selector: SystemComponent.SELECTOR, templateUrl: "./system.component.html", }) -export class SystemComponent implements OnInit { +export class SystemComponent { private static readonly SELECTOR = "system"; - public readonly environment = environment; - public readonly spinnerId: string = SystemComponent.SELECTOR; - public showLog: boolean = false; - public readonly ESTIMATED_REBOOT_TIME = 600; // Seconds till the openems service is restarted after update - - public edge: Edge; - public restartTime: number = this.ESTIMATED_REBOOT_TIME; - + protected readonly environment = environment; + protected readonly spinnerId: string = SystemComponent.SELECTOR; + protected showLog: boolean = false; + protected readonly ESTIMATED_REBOOT_TIME = 600; // Seconds till the openems service is restarted after update + protected edge: Edge; + protected restartTime: number = this.ESTIMATED_REBOOT_TIME; protected canSeeSystemRestart: boolean = false; constructor( - private route: ActivatedRoute, protected utils: Utils, private service: Service, - ) { } - - ngOnInit() { - this.service.getCurrentEdge().then(edge => { - this.edge = edge; - this.canSeeSystemRestart = UserPermission.isAllowedToSeeSystemRestart(this.service.currentUser, edge); + ) { + effect(async () => { + const user = this.service.currentUser(); + this.edge = await this.service.getCurrentEdge(); + this.canSeeSystemRestart = UserPermission.isAllowedToSeeSystemRestart(user, this.edge); }); } } diff --git a/ui/src/app/shared/components/chart/abstracthistorychart.ts b/ui/src/app/shared/components/chart/abstracthistorychart.ts index 8fed75139ef..5d4969e8cc8 100644 --- a/ui/src/app/shared/components/chart/abstracthistorychart.ts +++ b/ui/src/app/shared/components/chart/abstracthistorychart.ts @@ -363,9 +363,8 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { let options: Chart.ChartOptions = Utils.deepCopy(Utils.deepCopy(AbstractHistoryChart.getDefaultOptions(chartOptionsType, service, labels))); const displayValues: HistoryUtils.DisplayValue[] = chartObject.output(channelData.data); - const showYAxisType: boolean = chartObject.yAxes.length > 1; chartObject.yAxes.forEach((element) => { - options = AbstractHistoryChart.getYAxisOptions(options, element, translate, chartType, locale, datasets, showYAxisType); + options = AbstractHistoryChart.getYAxisOptions(options, element, translate, chartType, locale, datasets, true); }); options.plugins.tooltip.callbacks.title = (tooltipItems: Chart.TooltipItem[]): string => { @@ -475,7 +474,7 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { function rebuildScales(chart: Chart.Chart) { let options = chart.options; chartObject.yAxes.forEach((element) => { - options = AbstractHistoryChart.getYAxisOptions(options, element, translate, chartType, locale, _dataSets, showYAxisType); + options = AbstractHistoryChart.getYAxisOptions(options, element, translate, chartType, locale, _dataSets, true); }); } @@ -510,7 +509,6 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { options.scales.x.ticks["source"] = "auto"; options.scales.x.ticks.maxTicksLimit = 31; options.scales.x["bounds"] = "ticks"; - options; options = AbstractHistoryChart.getExternalPluginFeatures(displayValues, options, chartType); return options; @@ -597,13 +595,6 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { beginAtZero: false, }; break; - - case YAxisType.POWER: - case YAxisType.ENERGY: - case YAxisType.REACTIVE: - case YAxisType.NONE: - options.scales[element.yAxisId] = baseConfig; - break; case YAxisType.CURRENCY: options.scales[element.yAxisId] = { ...baseConfig, @@ -613,6 +604,12 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { }, }; break; + case YAxisType.POWER: + case YAxisType.ENERGY: + case YAxisType.REACTIVE: + case YAxisType.NONE: + options.scales[element.yAxisId] = baseConfig; + break; } return options; } diff --git a/ui/src/app/shared/components/chart/chart.constants.spec.ts b/ui/src/app/shared/components/chart/chart.constants.spec.ts index 3687e1d71fb..c107bc89f6c 100644 --- a/ui/src/app/shared/components/chart/chart.constants.spec.ts +++ b/ui/src/app/shared/components/chart/chart.constants.spec.ts @@ -27,7 +27,7 @@ describe("Chart constants", () => { ]; expect(ChartConstants.getScaleOptions([], yAxis, "line")).toEqual({ min: null, max: null, stepSize: null }); - expect(ChartConstants.getScaleOptions(datasets, yAxis, "line")).toEqual({ min: 0, max: 1892, stepSize: 378.4 }); + expect(ChartConstants.getScaleOptions(datasets, yAxis, "line")).toEqual({ min: 62, max: 1892, stepSize: 366 }); expect(ChartConstants.getScaleOptions(null, yAxis, "line")).toEqual({ min: null, max: null, stepSize: null }); expect(ChartConstants.getScaleOptions(null, null, "line")).toEqual({ min: null, max: null, stepSize: null }); }); diff --git a/ui/src/app/shared/components/chart/chart.constants.ts b/ui/src/app/shared/components/chart/chart.constants.ts index acae3146c98..3ed4294bb78 100644 --- a/ui/src/app/shared/components/chart/chart.constants.ts +++ b/ui/src/app/shared/components/chart/chart.constants.ts @@ -76,7 +76,7 @@ export class ChartConstants { return { title: { - text: element.customTitle ?? AbstractHistoryChart.getYAxisType(element.unit, translate, chartType), + text: element.customTitle ?? AbstractHistoryChart.getYAxisType(element.unit, translate, chartType, element.customTitle), display: false, padding: 5, font: { @@ -141,8 +141,8 @@ export class ChartConstants { return Object.values(stackMap) .reduce((arr: { min: number, max: number, stepSize: number }, dataset: ChartDataset) => { - - const min = Math.floor(Math.min(arr.min, ArrayUtils.findSmallestNumber(dataset.data as number[]))) ?? null; + const currMin = ArrayUtils.findSmallestNumber(dataset.data as number[]); + const min = Math.floor(Math.min(...[arr.min, currMin].filter(el => el != null))) ?? null; const max = Math.ceil(Math.max(arr.max, ArrayUtils.findBiggestNumber(dataset.data as number[]))) ?? null; if (max === null || min === null) { diff --git a/ui/src/app/shared/components/formly/formly-skeleton-wrapper.ts b/ui/src/app/shared/components/formly/formly-skeleton-wrapper.ts index 8dc332dcf29..5677e652ae9 100644 --- a/ui/src/app/shared/components/formly/formly-skeleton-wrapper.ts +++ b/ui/src/app/shared/components/formly/formly-skeleton-wrapper.ts @@ -10,7 +10,7 @@ import { FormlyFieldConfig } from "@ngx-formly/core"; * @input model the model */ @Component({ - selector: "formly-skeleton-wrapper", + selector: "oe-formly-skeleton-wrapper", template: `
diff --git a/ui/src/app/shared/service/service.ts b/ui/src/app/shared/service/service.ts index cc997a4d76b..73e2b1ea3d3 100644 --- a/ui/src/app/shared/service/service.ts +++ b/ui/src/app/shared/service/service.ts @@ -1,12 +1,12 @@ // @ts-strict-ignore import { registerLocaleData } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { Injectable, WritableSignal, signal } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { ToastController } from "@ionic/angular"; import { LangChangeEvent, TranslateService } from "@ngx-translate/core"; import { NgxSpinnerService } from "ngx-spinner"; import { BehaviorSubject, Subject } from "rxjs"; -import { filter, first, map, take } from "rxjs/operators"; +import { filter, first, take } from "rxjs/operators"; import { ChosenFilter } from "src/app/index/filter/filter.component"; import { environment } from "src/environments"; import { ChartConstants } from "../components/chart/chart.constants"; @@ -79,7 +79,7 @@ export class Service extends AbstractService { user: User, edges: { [edgeId: string]: Edge } }> = new BehaviorSubject(null); - public currentUser: User | null = null; + public currentUser: WritableSignal = signal(null); /** * Holds the current Activated Route @@ -186,24 +186,6 @@ export class Service extends AbstractService { }); } - /** - * Gets the current user - * - * @returns a Promise of the user - */ - public getCurrentUser(): Promise { - return new Promise((resolve) => { - this.metadata.pipe( - filter(metadata => metadata != null && metadata.user != null), - map(metadata => metadata.user), - first(), - ).toPromise().then(resolve); - if (this.currentUser) { - resolve(this.currentUser); - } - }); - } - public getConfig(): Promise { return new Promise((resolve, reject) => { this.getCurrentEdge().then(edge => { @@ -217,6 +199,7 @@ export class Service extends AbstractService { public onLogout() { this.currentEdge.next(null); this.metadata.next(null); + this.currentUser.set(null); this.websocket.state.set(States.NOT_AUTHENTICATED); this.router.navigate(["/login"]); } diff --git a/ui/src/app/shared/service/utils.ts b/ui/src/app/shared/service/utils.ts index 837e37acc54..4611de81c37 100644 --- a/ui/src/app/shared/service/utils.ts +++ b/ui/src/app/shared/service/utils.ts @@ -346,9 +346,9 @@ export class Utils { */ public static convertChargeDischargePower(translate: TranslateService, power: number): { name: string, value: number } { if (power >= 0) { - return { name: translate.instant("General.dischargePower"), value: power }; + return { name: translate.instant("General.DISCHARGE"), value: power }; } else { - return { name: translate.instant("General.chargePower"), value: power * -1 }; + return { name: translate.instant("General.CHARGE"), value: power * -1 }; } } diff --git a/ui/src/app/shared/service/websocket.ts b/ui/src/app/shared/service/websocket.ts index 6df2b0d368b..c3640de7e82 100644 --- a/ui/src/app/shared/service/websocket.ts +++ b/ui/src/app/shared/service/websocket.ts @@ -82,7 +82,7 @@ export class Websocket implements WebsocketInterface { // received login token -> save in cookie this.cookieService.set("token", authenticateResponse.token, { expires: 365, path: "/", sameSite: "Strict", secure: location.protocol === "https:" }); - this.service.currentUser = authenticateResponse.user; + this.service.currentUser.set(authenticateResponse.user); // Metadata this.service.metadata.next({ diff --git a/ui/src/app/user/user.component.html b/ui/src/app/user/user.component.html index 69df7a384f5..030bf9167d8 100644 --- a/ui/src/app/user/user.component.html +++ b/ui/src/app/user/user.component.html @@ -74,9 +74,9 @@ - - + + - + + diff --git a/ui/src/app/user/user.component.ts b/ui/src/app/user/user.component.ts index 694549cf3b6..a360e9741c3 100644 --- a/ui/src/app/user/user.component.ts +++ b/ui/src/app/user/user.component.ts @@ -1,5 +1,5 @@ // @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit, effect } from "@angular/core"; import { FormGroup, Validators } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { FormlyFieldConfig } from "@ngx-formly/core"; @@ -56,7 +56,7 @@ export class UserComponent implements OnInit { disabled: true, }, }]; - protected readonly companyInformationFields: FormlyFieldConfig[] = []; + protected companyInformationFields: FormlyFieldConfig[] = []; protected isAtLeastAdmin: boolean = false; @@ -65,92 +65,22 @@ export class UserComponent implements OnInit { public service: Service, private route: ActivatedRoute, private websocket: Websocket, - ) { } + ) { + effect(() => { + const user = this.service.currentUser(); + + if (user) { + this.isAtLeastAdmin = Role.isAtLeast(user.globalRole, Role.ADMIN); + this.updateUserInformation(); + } + }); + } ngOnInit() { // Set currentLanguage to this.currentLanguage = Language.getByKey(localStorage.LANGUAGE) ?? Language.DEFAULT; - this.service.getCurrentUser().then(user => { - this.isAtLeastAdmin = Role.isAtLeast(user.globalRole, Role.ADMIN); - }); - - this.getUserInformation().then((userInformation) => { - this.form = { - formGroup: new FormGroup({}), - model: userInformation, - }; - - const baseInformationFields: FormlyFieldConfig[] = [{ - key: "street", - type: "input", - props: { - label: this.translate.instant("Register.Form.street"), - disabled: true, - }, - }, - { - key: "zip", - type: "input", - props: { - label: this.translate.instant("Register.Form.zip"), - disabled: true, - }, - }, - { - key: "city", - type: "input", - props: { - label: this.translate.instant("Register.Form.city"), - disabled: true, - }, - }, - { - key: "country", - type: "select", - props: { - label: this.translate.instant("Register.Form.country"), - options: COUNTRY_OPTIONS(this.translate), - disabled: true, - }, - }, - { - key: "email", - type: "input", - props: { - label: this.translate.instant("Register.Form.email"), - disabled: true, - }, - validators: { - validation: [Validators.email], - }, - }, - { - key: "phone", - type: "input", - props: { - label: this.translate.instant("Register.Form.phone"), - disabled: true, - }, - }]; - - if (Object.prototype.hasOwnProperty.call(userInformation, "companyName")) { - this.companyInformationFields.push( - { - key: "companyName", - type: "input", - props: { - label: this.translate.instant("Register.Form.companyName"), - disabled: true, - }, - }, - ...baseInformationFields, - ); - } else { - this.userInformationFields.push(...baseInformationFields); - } - - }).then(() => { + this.updateUserInformation().then(() => { this.service.metadata.subscribe(entry => { this.showInformation = true; }); @@ -271,4 +201,82 @@ export class UserComponent implements OnInit { this.currentLanguage = language; this.translate.use(language.key); } + + private updateUserInformation(): Promise { + return this.getUserInformation().then((userInformation) => { + this.form = { + formGroup: new FormGroup({}), + model: userInformation, + }; + + const baseInformationFields: FormlyFieldConfig[] = [{ + key: "street", + type: "input", + props: { + label: this.translate.instant("Register.Form.street"), + disabled: true, + }, + }, + { + key: "zip", + type: "input", + props: { + label: this.translate.instant("Register.Form.zip"), + disabled: true, + }, + }, + { + key: "city", + type: "input", + props: { + label: this.translate.instant("Register.Form.city"), + disabled: true, + }, + }, + { + key: "country", + type: "select", + props: { + label: this.translate.instant("Register.Form.country"), + options: COUNTRY_OPTIONS(this.translate), + disabled: true, + }, + }, + { + key: "email", + type: "input", + props: { + label: this.translate.instant("Register.Form.email"), + disabled: true, + }, + validators: { + validation: [Validators.email], + }, + }, + { + key: "phone", + type: "input", + props: { + label: this.translate.instant("Register.Form.phone"), + disabled: true, + }, + + }]; + + if (Object.prototype.hasOwnProperty.call(userInformation, "companyName")) { + this.companyInformationFields = [{ + key: "companyName", + type: "input", + props: { + label: this.translate.instant("Register.Form.companyName"), + disabled: true, + }, + }, + ...baseInformationFields, + ]; + } else { + this.userInformationFields = baseInformationFields; + } + }); + } } diff --git a/ui/src/assets/i18n/cz.json b/ui/src/assets/i18n/cz.json index 04ac9dd63ec..7b90e504b9b 100644 --- a/ui/src/assets/i18n/cz.json +++ b/ui/src/assets/i18n/cz.json @@ -10,7 +10,6 @@ "changeAccepted": "Změna byla přijata", "changeFailed": "Změna se nezdařila", "chargeDischarge": "Debetní/vybíjení", - "chargePower": "Nabíjecí výkon", "componentCount": "Počet komponentů", "componentInactive": "Komponenta je neaktivní!", "connectionLost": "Spojení ztraceno. Pokouší se znovu připojit.", @@ -22,7 +21,6 @@ "digitalInputs": "Digitální vstupy", "numberOfComponents": "Počet komponentů", "directConsumption": "Přímá spotřeba", - "dischargePower": "Vybíjecí výkon", "fault": "Chyba", "grid": "Síť", "gridBuy": "Nákup ze sítě", @@ -96,7 +94,9 @@ "MINUTES": "Minuty", "DAY": "Den", "DAYS": "Dny" - } + }, + "DISCHARGE": "Vypouštění", + "CHARGE": "Načítání" }, "Menu": { "accessLevel": "Úroveň přístupu", diff --git a/ui/src/assets/i18n/de.json b/ui/src/assets/i18n/de.json index 954c76a43ce..2d1905b1c01 100644 --- a/ui/src/assets/i18n/de.json +++ b/ui/src/assets/i18n/de.json @@ -471,7 +471,6 @@ "changeAccepted": "Änderung übernommen", "changeFailed": "Änderung fehlgeschlagen", "chargeDischarge": "Be-/Entladung", - "chargePower": "Beladung", "componentCount": "Anzahl Komponenten", "componentInactive": "Komponente ist inaktiv!", "connectionLost": "Verbindung unterbrochen. Versuche die Verbindung wiederherzustellen.", @@ -483,7 +482,6 @@ "digitalInputs": "Digitaleingänge", "numberOfComponents": "Anzahl der Komponenten", "directConsumption": "Direktverbrauch", - "dischargePower": "Entladung", "energyLimit": "Energielimit", "fault": "Fehler", "grid": "Netz", @@ -574,7 +572,9 @@ "MINUTES": "Minuten", "DAY": "Tag", "DAYS": "Tage" - } + }, + "CHARGE": "Beladung", + "DISCHARGE": "Entladung" }, "Index": { "allConnected": "Alle Verbindungen hergestellt.", diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json index 0ce4b7372fb..c6d1adbbb34 100644 --- a/ui/src/assets/i18n/en.json +++ b/ui/src/assets/i18n/en.json @@ -471,8 +471,7 @@ "capacity": "Capacity", "changeAccepted": "Change accepted", "changeFailed": "Change failed", - "chargeDischarge": "Charge/Discharge power", - "chargePower": "Charge power", + "chargeDischarge": "Charge/Discharge", "componentCount": "Number of components", "componentInactive": "Component is not active!", "connectionLost": "Connection lost. Trying to reconnect.", @@ -484,7 +483,6 @@ "digitalInputs": "Digital Inputs", "numberOfComponents": "Number of Components", "directConsumption": "Direct consumption", - "dischargePower": "Discharge power", "fault": "Fault", "FAILED": "failed", "WILL_BE_EXECUTED": "will be executed", @@ -576,7 +574,9 @@ "MINUTES": "Minutes", "DAY": "Day", "DAYS": "Days" - } + }, + "CHARGE": "Charge", + "DISCHARGE": "Discharge" }, "Index": { "allConnected": "All connections established.", diff --git a/ui/src/assets/i18n/es.json b/ui/src/assets/i18n/es.json index 251dd41029d..68f7cbe885b 100644 --- a/ui/src/assets/i18n/es.json +++ b/ui/src/assets/i18n/es.json @@ -9,7 +9,6 @@ "changeAccepted": "Cambio aceptado", "changeFailed": "Cambio fallido", "chargeDischarge": "Débito/Descarga", - "chargePower": "Carga", "componentCount": "Numero de componentes", "componentInactive": "El componente está inactivo!", "connectionLost": "Conexión perdida. Intentando reconectar.", @@ -20,7 +19,6 @@ "digitalInputs": "Entradas digitales", "numberOfComponents": "número de componentes", "directConsumption": "Consumo directo", - "dischargePower": "Descarga", "fault": "Error", "grid": "Red", "gridBuy": "Relación", @@ -91,7 +89,9 @@ "MINUTES": "Minutos", "DAY": "Día", "DAYS": "Días" - } + }, + "DISCHARGE": "Descargar", + "CHARGE": "Cargando" }, "Menu": { "accessLevel": "Nivel de acceso", diff --git a/ui/src/assets/i18n/fr.json b/ui/src/assets/i18n/fr.json index 2ead4d8e8c0..2d3c86ae82d 100644 --- a/ui/src/assets/i18n/fr.json +++ b/ui/src/assets/i18n/fr.json @@ -11,7 +11,6 @@ "changeAccepted": "Changement accepté", "changeFailed": "Changement échoué", "chargeDischarge": "Puissance de Charge/Décharge", - "chargePower": "Puissance de charge", "componentInactive": "Component is not active!", "connectionLost": "Connection lost. Trying to reconnect.", "consumption": "Consommation", @@ -19,7 +18,6 @@ "currentName": "current name", "currentValue": "current value", "dateFormat": "yyyy-MM-dd", - "dischargePower": "Puissance de décharge", "digitalInputs": "Entrées numériques", "fault": "Fault", "grid": "Réseau", @@ -93,7 +91,9 @@ "MINUTES": "Minutes", "DAY": "Journée", "DAYS": "Journées" - } + }, + "DISCHARGE": "Décharge", + "CHARGE": "Charge" }, "Menu": { "accessLevel": "Niveau d'accès", diff --git a/ui/src/assets/i18n/ja.json b/ui/src/assets/i18n/ja.json index 06a7294eb4f..5ec3a322dd5 100644 --- a/ui/src/assets/i18n/ja.json +++ b/ui/src/assets/i18n/ja.json @@ -410,7 +410,6 @@ "changeAccepted": "変更が承認されました", "changeFailed": "変更が失敗しました", "chargeDischarge": "充電・放電パワー", - "chargePower": "充電", "componentCount": "部品数", "componentInactive": "無効化中", "connectionLost": "接続が切断されました。再接続を試みます。", @@ -422,7 +421,6 @@ "digitalInputs": "デジタル入力", "numberOfComponents": "部品数", "directConsumption": "直接消費", - "dischargePower": "放電", "fault": "障害", "grid": "グリッド", "gridBuy": "買電", @@ -503,7 +501,9 @@ "yes": "はい", "no": "いいえ", "value": "値", - "SUM_STATE": "システム状態" + "SUM_STATE": "システム状態", + "DISCHARGE": "放電", + "CHARGE": "充電" }, "Index": { "allConnected": "すべての接続が確立されています。", diff --git a/ui/src/assets/i18n/nl.json b/ui/src/assets/i18n/nl.json index 77d013c0482..8104de8fd3d 100644 --- a/ui/src/assets/i18n/nl.json +++ b/ui/src/assets/i18n/nl.json @@ -9,7 +9,6 @@ "changeAccepted": "Wijziging geaccepteerd", "changeFailed": "Wijziging mislukt", "chargeDischarge": "Debet/ontlaad", - "chargePower": "Laad vermogen", "componentCount": "Aantal componenten", "componentInactive": "Component is inactief!", "connectionLost": "Verbinding verbroken. Probeer opnieuw verbinding te maken.", @@ -20,7 +19,6 @@ "digitalInputs": "Digitale Ingangen", "numberOfComponents": "aantal componenten", "directConsumption": "Directe consumptie", - "dischargePower": "Ontlaad vermogen", "fault": "Fout", "grid": "Net", "gridBuy": "Netafname", @@ -88,7 +86,9 @@ "MINUTES": "Minuten", "DAY": "Dag", "DAYS": "Dagen" - } + }, + "DISCHARGE": "Afvoer", + "CHARGE": "Laden" }, "Menu": { "accessLevel": "Toegangsniveau",
General.chargePowerGeneral.CHARGE {{ data["_sum/EssDcChargeEnergy"] | unitvalue:'kWh' }}
General.dischargePowerGeneral.DISCHARGE {{ data["_sum/EssDcDischargeEnergy"] | unitvalue:'kWh' }}
General.chargePowerGeneral.CHARGE {{ (sum.effectivePower <= 0 ? (sum.effectivePower * -1) : null) | unitvalue:'W' }}
General.dischargePowerGeneral.DISCHARGE {{ (sum.effectivePower > 0 ? sum.effectivePower : null) | unitvalue:'W' }}
General.phase {{ phase }} General.dischargePower + translate>General.DISCHARGE {{ value | unitvalue:'W' }} @@ -61,7 +61,7 @@
General.phase {{ phase }} General.chargePower + translate>General.CHARGE {{ (value * -1) | unitvalue:'W' }} @@ -76,7 +76,7 @@
General.phase {{ phase }} General.dischargePower + translate>General.DISCHARGE {{ value | unitvalue:'W' }} @@ -85,7 +85,7 @@
General.phase {{ phase }} General.chargePower + translate>General.CHARGE {{ (value * -1) | unitvalue:'W' }} @@ -316,13 +316,13 @@
General.chargePowerGeneral.CHARGE {{ sum.effectiveChargePower | unitvalue:'W' }}
General.dischargePowerGeneral.DISCHARGE {{ sum.effectiveDischargePower | unitvalue:'W' }}
General.phase {{ phase }} - General.dischargePower + General.DISCHARGE {{ value | unitvalue:'W' }} @@ -349,7 +349,7 @@
General.phase {{ phase }} - General.chargePower + General.CHARGE {{ (value * -1) | unitvalue:'W' }} @@ -364,7 +364,7 @@
General.phase {{ phase }} - General.dischargePower + General.DISCHARGE {{ value | unitvalue:'W' }} @@ -373,7 +373,7 @@
General.phase {{ phase }} - General.chargePower + General.CHARGE {{ (value * -1) | unitvalue:'W' }} @@ -437,12 +437,12 @@ *ngIf="factory.natureIds.includes('io.openems.edge.ess.api.SymmetricEss') && value !== null && value !== undefined" class="full_width">
General.chargePowerGeneral.CHARGE {{ (value <= 0 ? (value * -1) : null) | unitvalue:'W' }}
General.dischargePowerGeneral.DISCHARGE {{ (value > 0 ? value : null) | unitvalue:'W' }} @@ -458,7 +458,7 @@ General.phase {{ phase }} - General.dischargePower + General.DISCHARGE {{ value | unitvalue:'W' }} @@ -467,7 +467,7 @@ General.phase {{ phase }} - General.chargePower + General.CHARGE {{ (value * -1) | unitvalue:'W' }} @@ -696,14 +696,14 @@ {{ component.alias }}
General.chargePowerGeneral.CHARGE {{ (value > 0 ? value : null) | unitvalue:'W' }}
General.dischargePowerGeneral.DISCHARGE {{ (value <= 0 ? (value * -1) : null | unitvalue:'W') }}