Skip to content

Commit

Permalink
FEMS Backports 2025-02-25 (#3024)
Browse files Browse the repository at this point in the history
* Edge
  * GoodWe: increase minimum power that will be ignored.
    * Bigger systems still have 79W permanent charge power, while the dynamic tariff would charge/discharge with zero.
    * After a longer discussion the values below 100W will be now ignored to avoid a lot of service cases that can not be explained.
  * **Clever-PV Controller**
    * New Controller `Controller.Clever-PV` sends core data to Clever-PV Push API every 15 seconds
    * See docs: https://www.clever-pv.com/anleitungen/push-api
  * Evcs: add helpers for phases
    * add  `Evcs:addCalculatePhasesListeners()`
    * add `Evcs:addCalculatePhasesFromActivePowerAndPhaseCurrents()`
    * Note: typical EVCS hardware models provide `CURRENT_L1-3`, and `ActivePower`,  but some models provide only `CURRENT_L1-3` and some other models provide only `ACTIVE_POWER_L1-3`. Therefore these methods are added to easily compute the missing channel values.
  * **Electrical Vehicle Supply Equipment (EVSE)**
    * This is WIP for a future replacement of the current EVCS APIs. The new namespace is "Electrical Vehicle Supply Equipment (EVSE)".
    * This PR provides:
      * APIs for
        * Electric Vehicle
        * Charge Point
      * Implementations:
        * KEBA P30/P40 via Modbus/TCP (`Evse.ChargePoint.Keba`)
        * Generic Electric Vehicle (`Evse.ElectricVehicle.Generic`)
      * Controllers:
        * Single Charge-Point (`Evse.Controller.Single`)
        * Cluster of Charge-Point-Controllers (`Evse.Controller.Cluster`)
      * Simple UI to control the Charge-Mode of Charge-Points
    * NOTE: All of this might be heavily refactored in future. Having it on develop helps us to test it easier in production. It does not affect any existing EVCS implementations.
  * BitsWordElement: Extend it via setToNullIfBit to indicate the invalid bit value input
    * Integrate the convert option for BitsWordELement
    * According to the given function/condition each bit channel values can be manipulated
    * The main reason of this implementation is to in order to indicate if value present or default value delivered
    * As an example; with this approach in case of default value detection, the values can be set to null
    * Similar approach was implemented in ElementToChannelConverter as "Set_to_null_for_default()" method
  * Implement UpdateUserSettingsRequest for edge-uiwebsocket
    * Adding updateUserSettingsRequest support for edge-ui-websocket request handler
    * Follow up to darkmode settings not being able to set locally
  * AppCenter: Only try to get Component if needed
    * If a component is not satisfied and therfor not startet an exception gets throw because the component can not be obtained
    * Solution: only get component if first method fails or can not provide the properties
  * KEBA: fix EnergyLimitReached when no Limit configured
    * Currently a bug starts to popup more and more in ticket.
    * -> Keba Charging Station has the Status ENERGY_LIMIT_REACHED eventhough energyLimit is not activated
    * Added removed check for limit != 0 (limit 0 should mean that no limit is set) in status mapping in ReadHandler of Keba

* UI:
  * time-of-use rename state to status
  * Dark-mode Popup
    * Displaying Popup after successful log in, to announce the Dark-mode new feature for users, showing it only once after users get the new software release.
  * Angular 19 migration fixes
  * setting chart afterTitle directly
  * Formly Radio Button
  * Add last setup protocol download to profile
    * Downloading latest setup protocol in `profile`
  * IOS Fixes
    * file download for excel export and last setup protocol did also not work on mobile with browser, now only *in app*  downloads are not working
    * theme selection popover radio buttons not shown, due to default ios design, changing to material design
    * -> before checkmark as radio icon with no circle, now with circle
  * Enforce datetime format for ion-datetime
    * fix datetime format of `ion-datetime`, ionic changed returned format of the date string
    * unit test to avoid this in future migrations
  * Change visibility of producttype and sumState filter
    * changing visibility of producttype and sumState filters in ems-overview from `admin` to `installer`
  * Enable darkmode locally & restrict float number decimals
    * Enabling setting darkmode for local monitoring
    * restrict float number decimals in y Axis ticks
  * Phase-accurate button change
    * Correcting buttons names on the footer navigation in history Consumption chart view.
  * Fix evcs modal range slider bug
    * Fixing ion-range bug

* Common/CI
  * Increased karma Timeout
  • Loading branch information
sfeilmeier authored Feb 26, 2025
1 parent a6dc8dc commit c37c1fb
Show file tree
Hide file tree
Showing 218 changed files with 6,229 additions and 1,356 deletions.
1 change: 1 addition & 0 deletions cnf/build.bnd
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ testpath: \
Edge_Controller_PVinverter;member=${filter;${p};io\.openems\.edge\.controller\.pvinverter\..*},\
Edge_Ess;member=${filter;${p};io\.openems\.edge\.ess\..*},\
Edge_Evcs;member=${filter;${p};io\.openems\.edge\.evcs\..*|io\.openems\.wrapper\.eu\.chargetime\.ocpp},\
Edge_Evse;member=${filter;${p};io\.openems\.edge\.evse\..*|io\.openems\.edge\.controller\.evse},\
Edge_Multiple;member=${filter;${p};io\.openems\.edge\.fenecon\..*|io\.openems\.edge\.goodwe|io\.openems\.edge\.kostal\.piko|io\.openems\.edge\.tesla\..*|io\.openems\.edge\.solaredge|io\.openems\.edge\.bosch\..*|io\.openems\.edge\.katek\..*|io\.openems\.edge\.kaco\..*},\
Edge_IO;member=${filter;${p};io\.openems\.edge\.io\..*|io\.openems\.edge\.controller\.channelthreshold|io\.openems\.edge\.controller\.chp\..*|io\.openems\.edge\.controller\.highloadtimeslot},\
Edge_Meter;member=${filter;${p};io\.openems\.edge\.meter\..*},\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public static <T> Supplier<T> supplier(Supplier<T> supplier) {
* the provided supplier. The value is computed and retrieved only once.
* Subsequent calls to {@link Supplier#get()} will return the cached value,
* avoiding recomputation.
*
* <p>
* This implementation is not thread-safe. If multiple threads invoke
* {@link Supplier#get()} concurrently, it may lead to inconsistent behavior,
Expand Down
9 changes: 9 additions & 0 deletions io.openems.edge.application/EdgeApp.bndrun
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
bnd.identity;id='io.openems.edge.controller.asymmetric.phaserectification',\
bnd.identity;id='io.openems.edge.controller.channelthreshold',\
bnd.identity;id='io.openems.edge.controller.chp.soc',\
bnd.identity;id='io.openems.edge.controller.cleverpv',\
bnd.identity;id='io.openems.edge.controller.debug.detailedlog',\
bnd.identity;id='io.openems.edge.controller.debug.log',\
bnd.identity;id='io.openems.edge.controller.ess.acisland',\
Expand All @@ -90,6 +91,7 @@
bnd.identity;id='io.openems.edge.controller.ess.timeofusetariff',\
bnd.identity;id='io.openems.edge.controller.evcs',\
bnd.identity;id='io.openems.edge.controller.evcs.fixactivepower',\
bnd.identity;id='io.openems.edge.controller.evse',\
bnd.identity;id='io.openems.edge.controller.generic.jsonlogic',\
bnd.identity;id='io.openems.edge.controller.highloadtimeslot',\
bnd.identity;id='io.openems.edge.controller.io.alarm',\
Expand Down Expand Up @@ -133,6 +135,8 @@
bnd.identity;id='io.openems.edge.evcs.spelsberg',\
bnd.identity;id='io.openems.edge.evcs.webasto.next',\
bnd.identity;id='io.openems.edge.evcs.webasto.unite',\
bnd.identity;id='io.openems.edge.evse.chargepoint.keba',\
bnd.identity;id='io.openems.edge.evse.electricvehicle.generic',\
bnd.identity;id='io.openems.edge.fenecon.dess',\
bnd.identity;id='io.openems.edge.fenecon.mini',\
bnd.identity;id='io.openems.edge.fenecon.pro',\
Expand Down Expand Up @@ -243,6 +247,7 @@
io.openems.edge.controller.asymmetric.phaserectification;version=snapshot,\
io.openems.edge.controller.channelthreshold;version=snapshot,\
io.openems.edge.controller.chp.soc;version=snapshot,\
io.openems.edge.controller.cleverpv;version=snapshot,\
io.openems.edge.controller.debug.detailedlog;version=snapshot,\
io.openems.edge.controller.debug.log;version=snapshot,\
io.openems.edge.controller.ess.acisland;version=snapshot,\
Expand All @@ -267,6 +272,7 @@
io.openems.edge.controller.ess.timeofusetariff;version=snapshot,\
io.openems.edge.controller.evcs;version=snapshot,\
io.openems.edge.controller.evcs.fixactivepower;version=snapshot,\
io.openems.edge.controller.evse;version=snapshot,\
io.openems.edge.controller.generic.jsonlogic;version=snapshot,\
io.openems.edge.controller.highloadtimeslot;version=snapshot,\
io.openems.edge.controller.io.alarm;version=snapshot,\
Expand Down Expand Up @@ -313,6 +319,9 @@
io.openems.edge.evcs.spelsberg;version=snapshot,\
io.openems.edge.evcs.webasto.next;version=snapshot,\
io.openems.edge.evcs.webasto.unite;version=snapshot,\
io.openems.edge.evse.api;version=snapshot,\
io.openems.edge.evse.chargepoint.keba;version=snapshot,\
io.openems.edge.evse.electricvehicle.generic;version=snapshot,\
io.openems.edge.fenecon.dess;version=snapshot,\
io.openems.edge.fenecon.mini;version=snapshot,\
io.openems.edge.fenecon.pro;version=snapshot,\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -33,6 +34,8 @@ private static record ChannelWrapper(Channel<Boolean> channel, BitConverter conv
/** Holds the ChannelWrapper; 'null' if not explicitly defined. */
private final ChannelWrapper[] channels = new ChannelWrapper[16];

private Function<Boolean[], Boolean[]> valueManipulator = Function.identity();

public BitsWordElement(int address, AbstractOpenemsModbusComponent component) {
super(OpenemsType.INTEGER, address);
this.component = component;
Expand All @@ -43,6 +46,7 @@ public BitsWordElement(int address, AbstractOpenemsModbusComponent component) {
value = new Boolean[16];
}

value = this.valueManipulator.apply(value);
for (var bitIndex = 0; bitIndex < 16; bitIndex++) {
// Get Wrapper
var wrapper = this.channels[bitIndex];
Expand All @@ -56,6 +60,17 @@ public BitsWordElement(int address, AbstractOpenemsModbusComponent component) {
});
}

/**
* Defines a convert method to manipulate the bit values.
*
* @param converter the defined function how to manipulate the value
* @return myself for builder pattern
*/
public BitsWordElement convert(Function<Boolean[], Boolean[]> converter) {
this.valueManipulator = converter;
return this;
}

@Override
protected BitsWordElement self() {
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.function.Function;

import org.junit.Test;

import com.ghgande.j2mod.modbus.procimg.Register;
Expand Down Expand Up @@ -103,6 +105,42 @@ public void testWriteBigEndian() throws Exception {
assertArrayEquals(new byte[] { (byte) 0x01, (byte) 0x06 }, registers[0].toBytes());
}

@Test
public void testSetToNull() throws Exception {
var sutTrue = generateSut();

final var channel0 = addBit(sutTrue, 0);// true
final var channel1 = addBit(sutTrue, 1);// null
final var channel2 = addBit(sutTrue, 2);// null
convert(sutTrue, value -> {
if (value[0] != null && value[0]) { // Reset all values if bit 0 is true
return new Boolean[16];
}
return value;
});
sutTrue.element.setInputValue(new Register[] { new SimpleRegister(7) }); // 0x7 = 111b
assertNull(channel0.getNextValue().get());
assertNull(channel1.getNextValue().get());
assertNull(channel2.getNextValue().get());

var sutFalse = generateSut();

final var channel3 = addBit(sutFalse, 0);// null
final var channel4 = addBit(sutFalse, 1);// false
final var channel5 = addBit(sutFalse, 2);// null
convert(sutFalse, value -> {
if (value[1] != null && !value[1]) {
return new Boolean[16]; // Reset all values if bit 1 is false
}
return value;
});

sutFalse.element.setInputValue(new Register[] { new SimpleRegister(5) }); // 0x5 = 101b
assertNull(channel3.getNextValue().get());
assertNull(channel4.getNextValue().get());
assertNull(channel5.getNextValue().get());
}

@Test(expected = IllegalArgumentException.class)
public void testRegistersLengthDoesNotMatch() throws Exception {
var sut = generateSut();
Expand Down Expand Up @@ -155,4 +193,9 @@ private static <T extends Channel<?>> T addBit(ModbusTest.FC3ReadRegisters<BitsW
}
return channel;
}

private static void convert(ModbusTest.FC3ReadRegisters<BitsWordElement, ?> sut,
Function<Boolean[], Boolean[]> converter) {
sut.element.convert(converter);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ public default Doc channelDoc() {
*
* @param value the 'next value'. It is going to be the 'value' after the next
* ProcessImage gets activated.
* @throws IllegalArgumentException on error
*/
public default void setNextValue(Object value) {
public default void setNextValue(Object value) throws IllegalArgumentException {
try {
this._setNextValue(TypeUtils.<T>getAsType(this.getType(), value));
} catch (IllegalArgumentException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,22 @@ private static <T extends Record> T getValues(OpenemsComponent component, Class<
}
}

/**
* Set next read value of a {@link Channel}.
*
* <p>
* Use this method as a short form for `this.channel(XYZ).setNextValue(value)`.
*
* @param component the {@link OpenemsComponent}
* @param channelId the {@link ChannelId}
* @param value value to be set
* @throws IllegalArgumentException on error
*/
public static void setValue(OpenemsComponent component, ChannelId channelId, Object value)
throws IllegalArgumentException {
component.channel(channelId).setNextValue(value);
}

/**
* Set write value of a {@link EnumWriteChannel} if the read value is not equal.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ public void updateChannelsBeforeProcessImage() {
// nothing here
}

/**
* Set {@link Sum.ChannelId#PRODUCTION_ACTIVE_POWER}.
*
* @param value the value
* @return myself
*/
public DummySum withProductionActivePower(int value) {
withValue(this, Sum.ChannelId.PRODUCTION_ACTIVE_POWER, value);
return this.self();
}

/**
* Set {@link Sum.ChannelId#PRODUCTION_AC_ACTIVE_POWER}.
*
Expand Down Expand Up @@ -71,6 +82,17 @@ public DummySum withEssSoc(int value) {
return this.self();
}

/**
* Set {@link Sum.ChannelId#ESS_ACTIVE_POWER}.
*
* @param value the value
* @return myself
*/
public DummySum withEssActivePower(Integer value) {
withValue(this, Sum.ChannelId.ESS_ACTIVE_POWER, value);
return this.self();
}

/**
* Set {@link Sum.ChannelId#ESS_MIN_DISCHARGE_POWER}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.openems.edge.controller.api.websocket.handler;

import org.osgi.service.component.annotations.Component;

import io.openems.common.jsonrpc.base.GenericJsonrpcResponseSuccess;
import io.openems.common.jsonrpc.request.UpdateUserSettingsRequest;
import io.openems.edge.common.jsonapi.JsonApi;
import io.openems.edge.common.jsonapi.JsonApiBuilder;

@Component(property = "entry=" + RootRequestHandler.ENTRY_POINT)
public class UserRequestHandler implements JsonApi {

@Override
public void buildJsonApiRoutes(JsonApiBuilder builder) {

builder.handleRequest(UpdateUserSettingsRequest.METHOD,
call -> new GenericJsonrpcResponseSuccess(call.getRequest().getId()));
}

}
12 changes: 12 additions & 0 deletions io.openems.edge.controller.cleverpv/.classpath
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="aQute.bnd.classpath.container"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21"/>
<classpathentry kind="src" output="bin" path="src"/>
<classpathentry kind="src" output="bin_test" path="test">
<attributes>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="bin"/>
</classpath>
2 changes: 2 additions & 0 deletions io.openems.edge.controller.cleverpv/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/bin_test/
/generated/
23 changes: 23 additions & 0 deletions io.openems.edge.controller.cleverpv/.project
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>io.openems.edge.controller.cleverpv</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>bndtools.core.bndbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>bndtools.core.bndnature</nature>
</natures>
</projectDescription>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8
encoding/bnd.bnd=UTF-8
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=21
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=21
19 changes: 19 additions & 0 deletions io.openems.edge.controller.cleverpv/bnd.bnd
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Bundle-Name: OpenEMS Edge Controller Clever-PV
Bundle-Vendor: FENECON GmbH
Bundle-License: https://opensource.org/licenses/EPL-2.0
Bundle-Version: 1.0.0.${tstamp}

-buildpath: \
${buildpath},\
Java-WebSocket,\
io.openems.common,\
io.openems.edge.bridge.http,\
io.openems.edge.common,\
io.openems.edge.controller.api,\
io.openems.edge.controller.api.common,\
io.openems.wrapper.okhttp,\

-testpath: \
${testpath},\
io.openems.wrapper.okhttp,\

7 changes: 7 additions & 0 deletions io.openems.edge.controller.cleverpv/readme.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
= Clever-PV

Sends current power data to the clever-PV cloud service via their Push-API.

See https://www.clever-pv.com/anleitungen/push-api for details and how to get the configuration URL.

https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.controller.cleverpv[Source Code icon:github[]]
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.openems.edge.controller.cleverpv;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(//
name = "Controller Clever-PV", //
description = "This controller connects to Clever-PV")
@interface Config {

@AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component")
String id() default "ctrlCleverPv0";

@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 = "URL", description = "Full API URL. See https://www.clever-pv.com/anleitungen/push-api", type = AttributeType.PASSWORD)
String url();

@AttributeDefinition(name = "Log-Verbosity", description = "The log verbosity.")
LogVerbosity logVerbosity() default LogVerbosity.NONE;

String webconsole_configurationFactory_nameHint() default "Controller Clever-PV [{id}]";
}
Loading

0 comments on commit c37c1fb

Please sign in to comment.