diff --git a/io.openems.common/src/io/openems/common/utils/JsonUtils.java b/io.openems.common/src/io/openems/common/utils/JsonUtils.java index 549872167c9..cc824a6e45e 100644 --- a/io.openems.common/src/io/openems/common/utils/JsonUtils.java +++ b/io.openems.common/src/io/openems/common/utils/JsonUtils.java @@ -971,6 +971,18 @@ public static String getAsStringOrElse(JsonElement jElement, String memberName, return getAsOptionalString(jElement, memberName).orElse(alternative); } + /** + * Gets the member of the {@link JsonElement} as {@link String} if it exists; or + * `null` otherwise. + * + * @param jElement the {@link JsonElement} + * @param memberName the name of the member + * @return the {@link String} value; or null + */ + public static String getAsStringOrNull(JsonElement jElement, String memberName) { + return toString(toPrimitive(toSubElement(jElement, memberName))); + } + /** * Converts a {@link JsonArray} to a String Array. * @@ -1044,6 +1056,18 @@ public static Optional getAsOptionalBoolean(JsonElement jElement, Strin return Optional.ofNullable(toBoolean(toPrimitive(toSubElement(jElement, memberName)))); } + /** + * Gets the member of the {@link JsonElement} as {@link Boolean} if it exists; + * or `null` otherwise. + * + * @param jElement the {@link JsonElement} + * @param memberName the name of the member + * @return the {@link Boolean} value; or null + */ + public static Boolean getAsBooleanOrNull(JsonElement jElement, String memberName) { + return toBoolean(toPrimitive(toSubElement(jElement, memberName))); + } + /** * Gets the {@link JsonElement} as short. * @@ -1165,6 +1189,18 @@ public static Optional getAsOptionalInt(JsonElement jElement, String me return Optional.ofNullable(toInt(toPrimitive(toSubElement(jElement, memberName)))); } + /** + * Gets the member of the {@link JsonElement} as {@link Integer} if it exists; + * or `null` otherwise. + * + * @param jElement the {@link JsonElement} + * @param memberName the name of the member + * @return the {@link Integer} value; or null + */ + public static Integer getAsIntOrNull(JsonElement jElement, String memberName) { + return toInt(toPrimitive(toSubElement(jElement, memberName))); + } + /** * Gets the {@link JsonElement} as long. * @@ -1322,6 +1358,18 @@ public static Optional getAsOptionalDouble(JsonElement jElement, String return Optional.ofNullable(toDouble(toPrimitive(toSubElement(jElement, memberName)))); } + /** + * Gets the member of the {@link JsonElement} as {@link Double} if it exists; or + * `null` otherwise. + * + * @param jElement the {@link JsonElement} + * @param memberName the name of the member + * @return the {@link Double} value; or null + */ + public static Double getAsDoubleOrNull(JsonElement jElement, String memberName) { + return toDouble(toPrimitive(toSubElement(jElement, memberName))); + } + /** * Gets the {@link JsonElement} as {@link Enum}. * diff --git a/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java b/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java index ce555d35ee0..be1f419f874 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java +++ b/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java @@ -505,17 +505,19 @@ public static Float multiply(Float... factors) { } /** - * Safely multiply Doubles. + * Safely multiply {@link Double}s. * - * @param factors the factors of the multiplication - * @return the result, possibly null if all factors are null + * @param firstFactor first factor of the multiplication + * @param furtherFactors further factors of the multiplication + * @return the result, possibly null if the first factor is null */ - public static Double multiply(Double... factors) { - Double result = null; - for (Double factor : factors) { - if (result == null) { - result = factor; - } else if (factor != null) { + public static Double multiply(Double firstFactor, Double... furtherFactors) { + if (firstFactor == null) { + return null; + } + double result = firstFactor; + for (Double factor : furtherFactors) { + if (factor != null) { result *= factor; } } diff --git a/io.openems.edge.common/test/io/openems/edge/common/type/TextProviderTest.java b/io.openems.edge.common/test/io/openems/edge/common/type/TextProviderTest.java index 7951940810c..953099ccdce 100644 --- a/io.openems.edge.common/test/io/openems/edge/common/type/TextProviderTest.java +++ b/io.openems.edge.common/test/io/openems/edge/common/type/TextProviderTest.java @@ -2,11 +2,12 @@ import static org.junit.Assert.assertEquals; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.openems.common.session.Language; public class TextProviderTest { + @Test public void testTextProvider() { var translationProvider = TextProvider.byTranslation(TextProviderTest.class, "Testing"); diff --git a/io.openems.edge.common/test/io/openems/edge/common/type/TypeUtilsTest.java b/io.openems.edge.common/test/io/openems/edge/common/type/TypeUtilsTest.java index f033f805a86..22ebb7aee33 100644 --- a/io.openems.edge.common/test/io/openems/edge/common/type/TypeUtilsTest.java +++ b/io.openems.edge.common/test/io/openems/edge/common/type/TypeUtilsTest.java @@ -10,8 +10,8 @@ import static io.openems.common.types.OpenemsType.STRING; import static io.openems.edge.common.type.TypeUtils.getAsJson; import static io.openems.edge.common.type.TypeUtils.sum; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import java.util.Optional; @@ -308,7 +308,7 @@ public void sumDouble() { private static void assertException(ThrowingRunnable runnable) { try { runnable.run(); - assertEquals("Expecting an Exception!", true, false); + assertEquals(true, false, "Expecting an Exception!"); } catch (Exception e) { // ok } diff --git a/io.openems.edge.evse.chargepoint.hardybarth/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.evse.chargepoint.hardybarth/.settings/org.eclipse.core.resources.prefs index 03ae24c14d3..2afafda39cb 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/.settings/org.eclipse.core.resources.prefs +++ b/io.openems.edge.evse.chargepoint.hardybarth/.settings/org.eclipse.core.resources.prefs @@ -1,6 +1,6 @@ eclipse.preferences.version=1 -encoding//test/io/openems/edge/evse/chargepoint/hardybarth/EvseChargePointHardyImplTest.java=UTF-8 -encoding//test/io/openems/edge/evse/chargepoint/hardybarth/MyConfig.java=UTF-8 +encoding//test/io/openems/edge/evse/chargepoint/hardybarth/cph2/EvseChargePointHardyImplTest.java=UTF-8 +encoding//test/io/openems/edge/evse/chargepoint/hardybarth/cph2/MyConfig.java=UTF-8 encoding/=UTF-8 encoding/bnd.bnd=UTF-8 encoding/readme.adoc=UTF-8 diff --git a/io.openems.edge.evse.chargepoint.hardybarth/readme.adoc b/io.openems.edge.evse.chargepoint.hardybarth/readme.adoc index 35d271ed5b3..76a5a0af66e 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/readme.adoc +++ b/io.openems.edge.evse.chargepoint.hardybarth/readme.adoc @@ -1,12 +1,13 @@ -= Hardy Barth Salia Charging Station += Hardy Barth Charging Stations -Three-phase AC charging station with REST API integration. +Three-phase AC charging stations with REST API integration. == Overview -This bundle provides integration with the Salia charging station by Hardy Barth, a three-phase AC electric vehicle charging station designed for commercial and residential charging applications. The station communicates via REST API over IP for flexible integration with energy management systems. +This bundle provides integration with Hardy Barth charging stations. Two device families are supported, each using a different REST API: -Hardy Barth Salia is suitable for commercial charging networks, workplace charging, and site energy management requiring three-phase charging integration. +* **Hardy Barth Salia** (`Evcs.HardyBarth`, `Evse.ChargePoint.HardyBarth`) – Salia controller API (`/api/secc`) +* **Hardy Barth cPH1 (ECB1)** (`Evse.ChargePoint.HardyBarth.cPH1`) – eCharge ECB1 REST API (`/api/v1/chargecontrols/`, `/api/v1/meters/`) == Supported Devices @@ -83,4 +84,64 @@ This bundle implements the following OpenEMS Components: [[_evcs_hardy]] +=== <<_evse_cph1,EVSE Charge-Point Hardy Barth cPH1>> + +*Name*: EVSE Charge-Point Hardy Barth cPH1 + +*Factory-PID*: `Evse.ChargePoint.HardyBarth.cPH1` + +.Implemented Natures/Interfaces +* EvseChargePointHardyBarthEcb1 +* OpenemsComponent +* EvseChargePoint +* ElectricityMeter + +*Description*: EVSE interface for the Hardy Barth eCharge cPH1 wallbox (EVCC Basic controller, ECB1 REST API). +Reads charge-control status and meter data from `/api/v1/chargecontrols/{id}` and `/api/v1/meters/{id}`. +Write operations (start, stop, set current) are dispatched as HTTP POST requests. + +.*Configuration*: + +* `id` (String): Component ID for unique identification (default: "evseChargePoint0") +* `alias` (String): Human-readable alias for the component +* `enabled` (Boolean): Enable/disable this component +* `ip` (String): IP address of the cPH1 wallbox (required, e.g. "192.168.2.8") +* `chargeControlId` (Integer): Charge-control unit ID (default: 1) +* `meterId` (Integer): Energy-meter ID (default: 1) +* `minHwCurrent` (Integer): Minimum hardware current in mA (default: 6000) +* `maxHwCurrent` (Integer): Maximum hardware current in mA (default: 32000) +* `phaseRotation` (PhaseRotation): Phase wiring configuration (default: L1_L2_L3) +* `readOnly` (Boolean): Read-only mode (monitoring only) vs managed control (default: false) + +.ECB1 API Endpoints Used +|=== +| Operation | Method | Endpoint + +| Read charge-control status +| GET +| `/api/v1/chargecontrols/{id}` + +| Read meter data +| GET +| `/api/v1/meters/{id}` + +| Set manual mode +| POST +| `/api/v1/chargecontrols/{id}/mode` (`mode=manual`) + +| Start charging +| POST +| `/api/v1/chargecontrols/{id}/start` + +| Stop charging +| POST +| `/api/v1/chargecontrols/{id}/stop` + +| Set current +| POST +| `/api/v1/chargecontrols/{id}/mode/manual/ampere` (`manualmodeamp={A}`) +|=== + +[[_evse_cph1]] + https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.evse.chargepoint.hardybarth[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/common/DeviceRole.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/common/DeviceRole.java index 67fc3976f8d..20cf886b318 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/common/DeviceRole.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/common/DeviceRole.java @@ -31,7 +31,7 @@ public enum DeviceRole { * {@link #UNKNOWN}. * * @param modelName salia device model name - * @param product raw device product + * @param product raw device product * @return device role */ public static DeviceRole fromModelNameAndProduct(String modelName, String product) { @@ -41,11 +41,13 @@ public static DeviceRole fromModelNameAndProduct(String modelName, String produc return result; } - if (modelName.toUpperCase().contains(DeviceRole.MASTER.name()) && product.trim().equals(DeviceRole.MASTER.product)) { + if (modelName.toUpperCase().contains(DeviceRole.MASTER.name()) + && product.trim().equals(DeviceRole.MASTER.product)) { result = MASTER; } - if (modelName.toUpperCase().contains(DeviceRole.SLAVE.name()) && product.trim().equals(DeviceRole.SLAVE.product)) { + if (modelName.toUpperCase().contains(DeviceRole.SLAVE.name()) + && product.trim().equals(DeviceRole.SLAVE.product)) { result = SLAVE; } diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/common/HardyBarth.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/common/HardyBarth.java index e1b45154c93..2a162af89b2 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/common/HardyBarth.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/common/HardyBarth.java @@ -27,8 +27,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId, PathP "secc", "port0", "ci", "evse", "basic", "grid_current_limit", "actual"), // RAW_PHASE_COUNT(Doc.of(INTEGER), // "secc", "port0", "ci", "evse", "basic", "phase_count"), // - RAW_PHYSICAL_CURRENT_LIMIT(Doc.of(STRING), - "secc", "port0", "ci", "evse", "basic", "physical_current_limit"), // + RAW_PHYSICAL_CURRENT_LIMIT(Doc.of(STRING), "secc", "port0", "ci", "evse", "basic", "physical_current_limit"), // // CHARGE RAW_CHARGE_STATUS_PLUG(Doc.of(STRING), // @@ -68,10 +67,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId, PathP "secc", "port0", "salia", "firmwareprogress"), // RAW_SALIA_PUBLISH(Doc.of(STRING), // "secc", "port0", "salia", "publish"), // - RAW_SALIA_SOCKET_MAX_AMP(Doc.of(STRING), - "secc", "port0", "salia", "socketmaxamp"), // - RAW_SALIA_INTCTRL_LIMIT(Doc.of(STRING), - "secc", "port0", "salia", "intctrl_limit"), // + RAW_SALIA_SOCKET_MAX_AMP(Doc.of(STRING), "secc", "port0", "salia", "socketmaxamp"), // + RAW_SALIA_INTCTRL_LIMIT(Doc.of(STRING), "secc", "port0", "salia", "intctrl_limit"), // RAW_SALIA_PHASE_SWITCHING_STATUS(Doc.of(STRING), // "secc", "port0", "salia", "phase_switching", "status"), // @@ -255,6 +252,7 @@ public default StringReadChannel getRawDeviceModelNameChannel() { /** * get Salia Device Model Name. + * * @return channel value */ public default String getSaliaDeviceModelName() { @@ -263,6 +261,7 @@ public default String getSaliaDeviceModelName() { /** * get Raw Device Product Channel. + * * @return the channel */ public default StringReadChannel getRawDeviceProductChannel() { @@ -271,6 +270,7 @@ public default StringReadChannel getRawDeviceProductChannel() { /** * get raw device product. + * * @return channel value */ public default String getRawDeviceProduct() { @@ -281,8 +281,8 @@ public default String getRawDeviceProduct() { * Reports whether the phase-switching API is available. * *

- * Detection is based on the raw - * {@code secc/port0/salia/phase_switching/status} value: + * Detection is based on the raw {@code secc/port0/salia/phase_switching/status} + * value: *

    *
  • {@code "idle"} and {@code "progress"} indicate support. *
  • A missing, null, undefined or any unknown value indicates no support. @@ -310,16 +310,15 @@ public default boolean hasPhaseSwitchingApi() { * Reports whether the phase-switching API is currently idle. * *

    - * This is true only if the raw - * {@code secc/port0/salia/phase_switching/status} value is exactly - * {@code "idle"}. A value of {@code "progress"} means a switch is already - * ongoing, so a new one must not be started; missing, null, undefined or - * unknown values return false. + * This is true only if the raw {@code secc/port0/salia/phase_switching/status} + * value is exactly {@code "idle"}. A value of {@code "progress"} means a switch + * is already ongoing, so a new one must not be started; missing, null, + * undefined or unknown values return false. * *

    - * This method evaluates only the API status. It does not verify the device - * role or communication freshness and does not guarantee that a subsequent - * write succeeds. Before controlling phase switching, callers must additionally + * This method evaluates only the API status. It does not verify the device role + * or communication freshness and does not guarantee that a subsequent write + * succeeds. Before controlling phase switching, callers must additionally * verify that {@link #deviceRole()} is {@link DeviceRole#SLAVE}. * * @return true if the last mapped status is exactly {@code "idle"} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ChargePointStatus.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/ChargePointStatus.java similarity index 92% rename from io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ChargePointStatus.java rename to io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/ChargePointStatus.java index b6cf45f662e..6751f46a5e0 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ChargePointStatus.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/ChargePointStatus.java @@ -1,4 +1,4 @@ -package io.openems.edge.evse.chargepoint.hardybarth; +package io.openems.edge.evse.chargepoint.hardybarth.cph2; import io.openems.common.types.OptionsEnum; diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/Config.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/Config.java similarity index 84% rename from io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/Config.java rename to io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/Config.java index 2658bbca3fe..35e018ec1e1 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/Config.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/Config.java @@ -1,4 +1,4 @@ -package io.openems.edge.evse.chargepoint.hardybarth; +package io.openems.edge.evse.chargepoint.hardybarth.cph2; import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; @@ -6,8 +6,8 @@ import io.openems.edge.evse.chargepoint.hardybarth.common.LogVerbosity; import io.openems.edge.meter.api.PhaseRotation; -@ObjectClassDefinition(name = "EVSE Charge-Point Hardy Barth", // - description = "The Hardy Barth P electric vehicle charging station") +@ObjectClassDefinition(name = "EVSE Charge-Point Hardy Barth cPH2", // + description = "The Hardy Barth cPH2 electric vehicle charging station with Salia") @interface Config { @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") @@ -31,5 +31,5 @@ @AttributeDefinition(name = "Log-Verbosity", description = "The log verbosity.") LogVerbosity logVerbosity() default LogVerbosity.NONE; - String webconsole_configurationFactory_nameHint() default "EVSE Charge-Point Hardy Barth [{id}]"; + String webconsole_configurationFactory_nameHint() default "EVSE Charge-Point Hardy Barth cPH2 [{id}]"; } \ No newline at end of file diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/EvseChargePointHardyBarth.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/EvseChargePointHardyBarth.java similarity index 95% rename from io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/EvseChargePointHardyBarth.java rename to io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/EvseChargePointHardyBarth.java index f632c4b5054..d4d34e6ac89 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/EvseChargePointHardyBarth.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/EvseChargePointHardyBarth.java @@ -1,4 +1,4 @@ -package io.openems.edge.evse.chargepoint.hardybarth; +package io.openems.edge.evse.chargepoint.hardybarth.cph2; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.channel.Doc; diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/EvseChargePointHardyBarthImpl.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/EvseChargePointHardyBarthImpl.java similarity index 98% rename from io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/EvseChargePointHardyBarthImpl.java rename to io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/EvseChargePointHardyBarthImpl.java index 0c44d65086b..49201c73c60 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/EvseChargePointHardyBarthImpl.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/EvseChargePointHardyBarthImpl.java @@ -1,4 +1,4 @@ -package io.openems.edge.evse.chargepoint.hardybarth; +package io.openems.edge.evse.chargepoint.hardybarth.cph2; import static io.openems.common.utils.FunctionUtils.doNothing; import static io.openems.edge.common.event.EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE; diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/EvseHandler.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/EvseHandler.java similarity index 98% rename from io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/EvseHandler.java rename to io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/EvseHandler.java index da59801aee5..c4ac7c66a36 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/EvseHandler.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/cph2/EvseHandler.java @@ -1,4 +1,4 @@ -package io.openems.edge.evse.chargepoint.hardybarth; +package io.openems.edge.evse.chargepoint.hardybarth.cph2; import static io.openems.common.types.OpenemsType.STRING; import static io.openems.edge.common.channel.ChannelUtils.setValue; diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Config.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Config.java new file mode 100644 index 00000000000..39e7fc740c3 --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Config.java @@ -0,0 +1,41 @@ +package io.openems.edge.evse.chargepoint.hardybarth.ecb1; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +import io.openems.edge.meter.api.PhaseRotation; + +@ObjectClassDefinition(// + name = "EVSE Charge-Point Hardy Barth cPH1", // + description = "Implements the Hardy Barth eCharge cPH1 electric vehicle charging station (ECB1 REST API).") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "evseChargePoint0"; + + @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 = "IP-Address", description = "The IP address of the charging station.", required = true) + String ip() default "192.168.2.8"; + + @AttributeDefinition(name = "Charge-Control-ID", description = "The ID of the charge control unit (default: 1).", required = true) + int chargeControlId() default 1; + + @AttributeDefinition(name = "Meter-ID", description = "The ID of the energy meter (default: 1).", required = true) + int meterId() default 1; + + @AttributeDefinition(name = "Maximum hardware current", description = "Maximum current of the Charger in mA.", required = true) + int maxHwCurrent() default 32000; + + @AttributeDefinition(name = "Phase Rotation", description = "Apply standard or rotated wiring") + PhaseRotation phaseRotation() default PhaseRotation.L1_L2_L3; + + @AttributeDefinition(name = "Read only", description = "Defines that this charger is read only.", required = true) + boolean readOnly() default false; + + String webconsole_configurationFactory_nameHint() default "EVSE Charge-Point Hardy Barth cPH1 [{id}]"; +} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Ecb1Handler.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Ecb1Handler.java new file mode 100644 index 00000000000..3d66c0440a1 --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Ecb1Handler.java @@ -0,0 +1,244 @@ +package io.openems.edge.evse.chargepoint.hardybarth.ecb1; + +import static io.openems.common.bridge.http.api.HttpMethod.POST; +import static io.openems.common.utils.JsonUtils.getAsBooleanOrNull; +import static io.openems.common.utils.JsonUtils.getAsDoubleOrNull; +import static io.openems.common.utils.JsonUtils.getAsIntOrNull; +import static io.openems.common.utils.JsonUtils.getAsStringOrNull; +import static io.openems.edge.common.channel.ChannelUtils.setValue; +import static io.openems.edge.common.type.TypeUtils.multiply; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.RejectedExecutionException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonObject; + +import io.openems.common.bridge.http.api.BridgeHttp; +import io.openems.common.bridge.http.api.BridgeHttpFactory; +import io.openems.common.bridge.http.api.HttpResponse; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.utils.JsonUtils; +import io.openems.common.utils.LatestWinsFutureExecutor; +import io.openems.edge.bridge.http.cycle.HttpBridgeCycleService; +import io.openems.edge.bridge.http.cycle.HttpBridgeCycleServiceDefinition; +import io.openems.edge.meter.api.ElectricityMeter; + +/** + * Handles all HTTP communication with the Hardy Barth cPH1 ECB1 REST API. + * + *

    + * Reads charge-control status and meter data every cycle and dispatches write + * commands (start / stop / set current) on demand. + */ +public class Ecb1Handler { + + private static final String OBIS_POWER_TOTAL = "1-0:1.4.0"; + private static final String OBIS_ENERGY_TOTAL = "1-0:1.8.0"; + private static final String OBIS_POWER_L1 = "1-0:21.4.0"; + private static final String OBIS_POWER_L2 = "1-0:41.4.0"; + private static final String OBIS_POWER_L3 = "1-0:61.4.0"; + private static final String OBIS_CURRENT_L1 = "1-0:31.4.0"; + private static final String OBIS_CURRENT_L2 = "1-0:51.4.0"; + private static final String OBIS_CURRENT_L3 = "1-0:71.4.0"; + private static final String OBIS_VOLTAGE_L1 = "1-0:32.4.0"; + private static final String OBIS_VOLTAGE_L2 = "1-0:52.4.0"; + private static final String OBIS_VOLTAGE_L3 = "1-0:72.4.0"; + + private final Logger log = LoggerFactory.getLogger(Ecb1Handler.class); + + private final Ecb1Parent parent; + private final String baseUrl; + private final int chargeControlId; + private final BridgeHttpFactory httpBridgeFactory; + private final BridgeHttp httpBridge; + private final HttpBridgeCycleService cycleService; + private final LatestWinsFutureExecutor targetExecutor = new LatestWinsFutureExecutor(); + + /** Last target current sent to the device (A), or -1 if unknown. */ + private int lastTargetCurrentA = -1; + + public Ecb1Handler(Ecb1Parent parent, String ip, int chargeControlId, int meterId, + BridgeHttpFactory httpBridgeFactory, HttpBridgeCycleServiceDefinition cycleServiceDef) { + this.parent = parent; + this.baseUrl = "http://" + ip + "/api/v1"; + this.chargeControlId = chargeControlId; + this.httpBridgeFactory = httpBridgeFactory; + this.httpBridge = httpBridgeFactory.get(); + this.cycleService = this.httpBridge.createService(cycleServiceDef); + + // Subscribe for charge-control status (every cycle) + this.cycleService.subscribeCycle(1, // + this.baseUrl + "/chargecontrols/" + chargeControlId, // + response -> this.handleChargeControlResponse(response.data()), // + error -> this.parent.onCommunicationFailed(true)); + + // Subscribe for meter data (every cycle) + this.cycleService.subscribeCycle(1, // + this.baseUrl + "/meters/" + meterId, // + response -> this.handleMeterResponse(response.data()), // + error -> this.log.warn("Failed to read ECB1 meter: " + error)); + + // Set manual mode on startup + this.setManualMode(); + } + + /** + * Releases the HTTP bridge on deactivation. + */ + public void deactivate() { + this.targetExecutor.cancel(); + this.httpBridgeFactory.unget(this.httpBridge); + } + + /** + * Sets the charge target current. Call with 0 to stop charging. + * + * @param currentA target current in Ampere (0 = stop) + * @return true if accepted for dispatch; false if the executor was cancelled + */ + public boolean setTarget(int currentA) { + if (currentA == this.lastTargetCurrentA) { + return true; + } + try { + this.targetExecutor.execute(// + () -> this.dispatchTarget(currentA), // + (response, error) -> { + if (error == null) { + this.lastTargetCurrentA = currentA; + } + }); + return true; + } catch (RejectedExecutionException e) { + return false; + } + } + + private CompletableFuture> dispatchTarget(int currentA) { + if (currentA == 0) { + return this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/stop") // + .setMethod(POST) // + .setBodyFormEncoded(Map.of()) // + .build()); + } + this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/mode/manual/ampere") // + .setMethod(POST) // + .setBodyFormEncoded(Map.of("manualmodeamp", String.valueOf(currentA))) // + .build()); + return this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/start") // + .setMethod(POST) // + .setBodyFormEncoded(Map.of()) // + .build()); + } + + /** + * Sends a one-shot request to set the ECB1 charge mode to "manual". + */ + private void setManualMode() { + this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/mode") // + .setMethod(POST) // + .setBodyFormEncoded(Map.of("mode", "manual")) // + .build()); + } + + private String chargeControlUrl() { + return this.baseUrl + "/chargecontrols/" + this.chargeControlId; + } + + // ------------------------------------------------------------------------- + // Response handlers + // ------------------------------------------------------------------------- + + /** + * Parses a GET /api/v1/chargecontrols/{id} response and updates channels. + * + * @param body raw JSON response body + */ + public void handleChargeControlResponse(String body) { + JsonObject json; + try { + json = JsonUtils.parseToJsonObject(body); + } catch (Exception e) { + this.log.warn("Cannot parse ECB1 charge-control response: " + e.getMessage()); + json = new JsonObject(); + } + + final var cc = JsonUtils.getAsOptionalJsonObject(json, "chargecontrol").orElse(null); + final var hb = this.parent; + + final var stateId = getAsIntOrNull(cc, "stateid"); + setValue(hb, EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE_ID, stateId); + final var state = getAsStringOrNull(cc, "state"); + setValue(hb, EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE, state); + final var mode = getAsStringOrNull(cc, "mode"); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_MODE, mode); + final var connected = getAsBooleanOrNull(cc, "connected"); + setValue(hb, EvseChargePointHardyBarthEcb1.ChannelId.RAW_CONNECTED, connected); + setValue(hb, EvseChargePointHardyBarthEcb1.ChannelId.RAW_MANUAL_MODE_AMP, + getAsDoubleOrNull(cc, "manualmodeamp")); + setValue(hb, EvseChargePointHardyBarthEcb1.ChannelId.RAW_CURRENT_PWM_AMP, + getAsDoubleOrNull(cc, "currentpwmamp")); + setValue(hb, EvseChargePointHardyBarthEcb1.ChannelId.RAW_VENDOR, getAsStringOrNull(cc, "vendor")); + setValue(hb, EvseChargePointHardyBarthEcb1.ChannelId.RAW_VERSION, getAsStringOrNull(cc, "version")); + + // Re-set manual mode if the device has drifted to another mode + if (mode != null && !mode.equals("manual")) { + this.setManualMode(); + } + + hb.onCommunicationFailed(false); + hb.onChargeControlStatus(state, stateId, connected); + } + + /** + * Parses a GET /api/v1/meters/{id} response and updates ElectricityMeter + * channels. + * + *

    + * OBIS values are in SI base units: W for power, A for current, V for voltage, + * Wh for energy. The ElectricityMeter channels expect mA for current and mV for + * voltage, so we multiply by 1000 before setting. + * + * @param body raw JSON response body + */ + public void handleMeterResponse(String body) { + JsonObject json; + try { + json = JsonUtils.parseToJsonObject(body); + } catch (OpenemsNamedException e) { + this.log.warn("Cannot parse ECB1 meter response: " + e.getMessage()); + json = new JsonObject(); + } + + final var meter = JsonUtils.getAsOptionalJsonObject(json, "meter").orElse(null); + final var data = JsonUtils.getAsOptionalJsonObject(meter, "data").orElse(null); + final var hb = this.parent; + + setValue(hb, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_SERIAL, getAsIntOrNull(meter, "serial")); + setValue(hb, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, getAsStringOrNull(meter, "vendor")); + setValue(hb, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_TYPE, getAsStringOrNull(meter, "type")); + + // Active power (W) + setValue(hb, ElectricityMeter.ChannelId.ACTIVE_POWER, getAsDoubleOrNull(data, OBIS_POWER_TOTAL)); + setValue(hb, ElectricityMeter.ChannelId.ACTIVE_POWER_L1, getAsDoubleOrNull(data, OBIS_POWER_L1)); + setValue(hb, ElectricityMeter.ChannelId.ACTIVE_POWER_L2, getAsDoubleOrNull(data, OBIS_POWER_L2)); + setValue(hb, ElectricityMeter.ChannelId.ACTIVE_POWER_L3, getAsDoubleOrNull(data, OBIS_POWER_L3)); + + // Current (A → mA) + setValue(hb, ElectricityMeter.ChannelId.CURRENT_L1, multiply(getAsDoubleOrNull(data, OBIS_CURRENT_L1), 1000.0)); + setValue(hb, ElectricityMeter.ChannelId.CURRENT_L2, multiply(getAsDoubleOrNull(data, OBIS_CURRENT_L2), 1000.0)); + setValue(hb, ElectricityMeter.ChannelId.CURRENT_L3, multiply(getAsDoubleOrNull(data, OBIS_CURRENT_L3), 1000.0)); + + // Voltage (V → mV) + setValue(hb, ElectricityMeter.ChannelId.VOLTAGE_L1, multiply(getAsDoubleOrNull(data, OBIS_VOLTAGE_L1), 1000.0)); + setValue(hb, ElectricityMeter.ChannelId.VOLTAGE_L2, multiply(getAsDoubleOrNull(data, OBIS_VOLTAGE_L2), 1000.0)); + setValue(hb, ElectricityMeter.ChannelId.VOLTAGE_L3, multiply(getAsDoubleOrNull(data, OBIS_VOLTAGE_L3), 1000.0)); + + // Energy (Wh) + setValue(hb, ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, getAsDoubleOrNull(data, OBIS_ENERGY_TOTAL)); + } +} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Ecb1Parent.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Ecb1Parent.java new file mode 100644 index 00000000000..c1c6e7d5159 --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Ecb1Parent.java @@ -0,0 +1,23 @@ +package io.openems.edge.evse.chargepoint.hardybarth.ecb1; + +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.meter.api.ElectricityMeter; + +public interface Ecb1Parent extends OpenemsComponent, ElectricityMeter { + + /** + * Called once per cycle when a chargecontrol response is successfully parsed. + * + * @param state IEC 61851 state string (A/B/C/D/E/F), may be null + * @param stateId numeric state ID, may be null + * @param connected true when a vehicle is plugged in, may be null + */ + void onChargeControlStatus(String state, Integer stateId, Boolean connected); + + /** + * Called when the ECB1 communication status changes. + * + * @param failed true if communication is currently failing + */ + void onCommunicationFailed(boolean failed); +} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1.java new file mode 100644 index 00000000000..a0f56b40fe2 --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1.java @@ -0,0 +1,88 @@ +package io.openems.edge.evse.chargepoint.hardybarth.ecb1; + +import io.openems.common.channel.Unit; +import io.openems.common.types.OpenemsType; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.evse.api.chargepoint.EvseChargePoint; +import io.openems.edge.meter.api.ElectricityMeter; + +public interface EvseChargePointHardyBarthEcb1 extends OpenemsComponent, EvseChargePoint, ElectricityMeter { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + /** + * ECB1 state ID. + * + *

      + *
    • 5 = Charging + *
    • 17 = Stopped / paused + *
    + */ + RAW_STATE_ID(Doc.of(OpenemsType.INTEGER)// + .text("ECB1 state ID (5=charging, 17=stopped)")), + + /** + * ECB1 IEC 61851 charge-point state. + * + *
      + *
    • A = No vehicle connected + *
    • B = Vehicle connected, not charging + *
    • C = Charging + *
    • D = Charging with ventilation + *
    • E = Deactivated socket + *
    • F = Fault + *
    + */ + RAW_STATE(Doc.of(OpenemsType.STRING)// + .text("ECB1 IEC 61851 state (A/B/C/D/E/F)")), + + /** ECB1 charge mode (e.g. "manual"). */ + RAW_MODE(Doc.of(OpenemsType.STRING)// + .text("ECB1 charge mode")), + + /** Actual PWM current amplitude in Ampere. */ + RAW_CURRENT_PWM_AMP(Doc.of(OpenemsType.DOUBLE)// + .unit(Unit.AMPERE)// + .text("ECB1 current PWM amplitude")), + + /** Manual-mode current setpoint in Ampere. */ + RAW_MANUAL_MODE_AMP(Doc.of(OpenemsType.DOUBLE)// + .unit(Unit.AMPERE)// + .text("ECB1 manual-mode current setpoint")), + + /** True when a vehicle is plugged in. */ + RAW_CONNECTED(Doc.of(OpenemsType.BOOLEAN)// + .text("Vehicle connected")), + + /** EVCC vendor name. */ + RAW_VENDOR(Doc.of(OpenemsType.STRING)// + .text("EVCC vendor")), + + /** EVCC firmware version. */ + RAW_VERSION(Doc.of(OpenemsType.STRING)// + .text("EVCC firmware version")), + + /** Meter serial number. */ + RAW_METER_SERIAL(Doc.of(OpenemsType.INTEGER)// + .text("Meter serial number")), + + /** Meter vendor. */ + RAW_METER_VENDOR(Doc.of(OpenemsType.STRING)// + .text("Meter vendor")), + + /** Meter type. */ + RAW_METER_TYPE(Doc.of(OpenemsType.STRING)// + .text("Meter type")); + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } +} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1Impl.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1Impl.java new file mode 100644 index 00000000000..576eb1f28be --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1Impl.java @@ -0,0 +1,129 @@ +package io.openems.edge.evse.chargepoint.hardybarth.ecb1; + +import static io.openems.edge.common.channel.ChannelUtils.setValue; +import static io.openems.edge.evcs.api.Evcs.evaluatePhaseCountFromCurrent; +import static org.osgi.service.component.annotations.ConfigurationPolicy.REQUIRE; + +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.metatype.annotations.Designate; + +import io.openems.common.bridge.http.api.BridgeHttpFactory; +import io.openems.edge.bridge.http.cycle.HttpBridgeCycleServiceDefinition; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.type.Phase; +import io.openems.edge.evse.api.chargepoint.EvseChargePoint; +import io.openems.edge.evse.api.chargepoint.Profile.ChargePointAbilities; +import io.openems.edge.evse.api.chargepoint.Profile.ChargePointActions; +import io.openems.edge.evse.api.common.ApplySetPoint; +import io.openems.edge.meter.api.ElectricityMeter; +import io.openems.edge.meter.api.PhaseRotation; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Evse.ChargePoint.HardyBarth.cPH1", // + immediate = true, // + configurationPolicy = REQUIRE // +) +public class EvseChargePointHardyBarthEcb1Impl extends AbstractOpenemsComponent + implements EvseChargePointHardyBarthEcb1, Ecb1Parent, OpenemsComponent, EvseChargePoint, ElectricityMeter { + + private static final int MIN_CURRENT_A = 6; + + @Reference + private BridgeHttpFactory httpBridgeFactory; + + @Reference + private HttpBridgeCycleServiceDefinition httpBridgeCycleServiceDefinition; + + private Config config; + private Ecb1Handler handler; + private boolean isEvConnected = false; + + public EvseChargePointHardyBarthEcb1Impl() { + super(// + OpenemsComponent.ChannelId.values(), // + ElectricityMeter.ChannelId.values(), // + EvseChargePoint.ChannelId.values(), // + EvseChargePointHardyBarthEcb1.ChannelId.values() // + ); + ElectricityMeter.calculateSumCurrentFromPhases(this); + ElectricityMeter.calculateAverageVoltageFromPhases(this); + } + + @Activate + private void activate(ComponentContext context, Config config) { + this.config = config; + super.activate(context, config.id(), config.alias(), config.enabled()); + + if (!this.isEnabled()) { + return; + } + + this.handler = new Ecb1Handler(this, config.ip(), config.chargeControlId(), config.meterId(), + this.httpBridgeFactory, this.httpBridgeCycleServiceDefinition); + } + + @Deactivate + protected void deactivate() { + super.deactivate(); + if (this.handler != null) { + this.handler.deactivate(); + } + } + + @Override + public void onChargeControlStatus(String state, Integer stateId, Boolean connected) { + final var isReady = state != null && !state.isEmpty() // + && (state.charAt(0) == 'B' || state.charAt(0) == 'C' || state.charAt(0) == 'D'); + this.isEvConnected = Boolean.TRUE.equals(connected); + setValue(this, EvseChargePoint.ChannelId.IS_READY_FOR_CHARGING, isReady); + } + + @Override + public void onCommunicationFailed(boolean failed) { + // No dedicated communication-failed channel in EVSE + } + + @Override + public ChargePointAbilities getChargePointAbilities() { + if (this.isReadOnly()) { + return ChargePointAbilities.create().build(); + } + + final var phaseCount = evaluatePhaseCountFromCurrent(// + this.getCurrentL1().orElse(0), // + this.getCurrentL2().orElse(0), // + this.getCurrentL3().orElse(0)); + final var phase = phaseCount != null && phaseCount == 1 // + ? Phase.SingleOrThreePhase.SINGLE_PHASE // + : Phase.SingleOrThreePhase.THREE_PHASE; + + return ChargePointAbilities.create() // + .setApplySetPoint(new ApplySetPoint.Ability.Ampere(phase, // + MIN_CURRENT_A, // + this.config.maxHwCurrent() / 1000)) // + .setIsEvConnected(this.isEvConnected) // + .setIsReadyForCharging(this.getIsReadyForCharging()) // + .build(); + } + + @Override + public void apply(ChargePointActions actions) { + this.handler.setTarget(actions.getApplySetPointInAmpere().value()); + } + + @Override + public PhaseRotation getPhaseRotation() { + return this.config.phaseRotation(); + } + + @Override + public boolean isReadOnly() { + return this.config.readOnly(); + } +} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImplTest.java b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImplTest.java index a765160d80a..4ecec3b4378 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImplTest.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/EvcsHardyBarthImplTest.java @@ -9,8 +9,8 @@ import static io.openems.edge.evse.chargepoint.hardybarth.common.TestData.PHASE_SWITCHING_MISSING; import static io.openems.edge.evse.chargepoint.hardybarth.common.TestData.PHASE_SWITCHING_STATUS_IDLE; import static io.openems.edge.meter.api.PhaseRotation.L2_L3_L1; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; @@ -319,8 +319,8 @@ void testHandleUndefinedCheck() throws Exception { /** * Lightweight check that {@link HardyBarth#hasPhaseSwitchingApi()} and - * {@link HardyBarth#canStartPhaseSwitch()} are available on this - * architecture too; the detailed mapping/interpretation is covered by + * {@link HardyBarth#canStartPhaseSwitch()} are available on this architecture + * too; the detailed mapping/interpretation is covered by * {@code EvseChargePointHardyImplTest}. */ @Test @@ -345,8 +345,8 @@ void testHasPhaseSwitchingApiSharedBehavior() throws Exception { var rh = ReflectionUtils.getValueViaReflection(sut, "handler"); test.next(new TestCase() // - .onBeforeProcessImage( - () -> rh.handleGetApiCallResponse(HttpResponse.ok(PHASE_SWITCHING_STATUS_IDLE), phaseRotation))); + .onBeforeProcessImage(() -> rh.handleGetApiCallResponse(HttpResponse.ok(PHASE_SWITCHING_STATUS_IDLE), + phaseRotation))); assertTrue(sut.hasPhaseSwitchingApi()); assertTrue(sut.canStartPhaseSwitch()); diff --git a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/common/TestData.java b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/common/TestData.java index 189b2bb8439..4051ac1edea 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/common/TestData.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/common/TestData.java @@ -347,82 +347,82 @@ public class TestData { """; public static final String PHASE_SWITCHING_STATUS_IDLE = """ - { - "secc":{ - "port0":{ - "salia":{ - "phase_switching":{ - "status":"idle" - } - } - } + { + "secc":{ + "port0":{ + "salia":{ + "phase_switching":{ + "status":"idle" } } - """; + } + } + } + """; public static final String PHASE_SWITCHING_STATUS_PROGRESS = """ - { - "secc":{ - "port0":{ - "salia":{ - "phase_switching":{ - "status":"progress" - } - } - } + { + "secc":{ + "port0":{ + "salia":{ + "phase_switching":{ + "status":"progress" } } - """; + } + } + } + """; public static final String PHASE_SWITCHING_STATUS_UNKNOWN = """ - { - "secc":{ - "port0":{ - "salia":{ - "phase_switching":{ - "status":"error" - } - } - } + { + "secc":{ + "port0":{ + "salia":{ + "phase_switching":{ + "status":"error" } } - """; + } + } + } + """; public static final String PHASE_SWITCHING_STATUS_NULL = """ - { - "secc":{ - "port0":{ - "salia":{ - "phase_switching":{ - "status":null - } - } - } + { + "secc":{ + "port0":{ + "salia":{ + "phase_switching":{ + "status":null } } - """; + } + } + } + """; public static final String PHASE_SWITCHING_NULL = """ - { - "secc":{ - "port0":{ - "salia":{ - "phase_switching":null - } - } - } + { + "secc":{ + "port0":{ + "salia":{ + "phase_switching":null } - """; + } + } + } + """; public static final String PHASE_SWITCHING_MISSING = """ - { - "secc":{ - "port0":{ - "salia":{ - } - } - } + { + "secc":{ + "port0":{ + "salia":{ } - """; + } + } + } + """; } diff --git a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/EvseChargePointHardyImplTest.java b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/cph2/EvseChargePointHardyImplTest.java similarity index 98% rename from io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/EvseChargePointHardyImplTest.java rename to io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/cph2/EvseChargePointHardyImplTest.java index fb486a8118f..c5700d0a1e8 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/EvseChargePointHardyImplTest.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/cph2/EvseChargePointHardyImplTest.java @@ -1,4 +1,4 @@ -package io.openems.edge.evse.chargepoint.hardybarth; +package io.openems.edge.evse.chargepoint.hardybarth.cph2; import static io.openems.common.bridge.http.dummy.DummyBridgeHttpFactory.ofBridgeImpl; import static io.openems.edge.common.test.TestUtils.withValue; @@ -202,8 +202,8 @@ void testStatus(String name, String json, String expectedRawValue, boolean expec boolean expectedCanStart) throws Exception { final var sut = generateSut(); sut.test.next(new TestCase() // - .onBeforeProcessImage( - () -> sut.evseHandler.handleGetApiCallResponse(HttpResponse.ok(json), PhaseRotation.L1_L2_L3)) // + .onBeforeProcessImage(() -> sut.evseHandler // + .handleGetApiCallResponse(HttpResponse.ok(json), PhaseRotation.L1_L2_L3)) // .output(HardyBarth.ChannelId.RAW_SALIA_PHASE_SWITCHING_STATUS, expectedRawValue) // // An undefined or unknown value must not trigger a warning/fault channel. .output(OpenemsComponent.ChannelId.STATE, Level.OK) // diff --git a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/MyConfig.java b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/cph2/MyConfig.java similarity index 96% rename from io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/MyConfig.java rename to io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/cph2/MyConfig.java index 3f6d464e59f..9f02bd8933b 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/MyConfig.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/cph2/MyConfig.java @@ -1,4 +1,4 @@ -package io.openems.edge.evse.chargepoint.hardybarth; +package io.openems.edge.evse.chargepoint.hardybarth.cph2; import io.openems.common.test.AbstractComponentConfig; import io.openems.edge.evse.chargepoint.hardybarth.common.LogVerbosity; diff --git a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1ImplTest.java b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1ImplTest.java new file mode 100644 index 00000000000..bfb8059724f --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1ImplTest.java @@ -0,0 +1,509 @@ +package io.openems.edge.evse.chargepoint.hardybarth.ecb1; + +import static io.openems.common.bridge.http.dummy.DummyBridgeHttpFactory.ofBridgeImpl; +import static io.openems.edge.common.test.TestUtils.withValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import io.openems.common.bridge.http.api.HttpResponse; +import io.openems.common.bridge.http.dummy.DummyBridgeHttpBundle; +import io.openems.common.bridge.http.dummy.DummyBridgeHttpFactory; +import io.openems.common.channel.Level; +import io.openems.common.utils.ReflectionUtils; +import io.openems.edge.bridge.http.cycle.HttpBridgeCycleServiceDefinition; +import io.openems.edge.bridge.http.cycle.dummy.DummyCycleSubscriber; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.common.type.Phase; +import io.openems.edge.evcs.api.Evcs; +import io.openems.edge.evse.api.chargepoint.EvseChargePoint; +import io.openems.edge.evse.api.chargepoint.Profile.ChargePointActions; +import io.openems.edge.evse.api.common.ApplySetPoint; +import io.openems.edge.meter.api.ElectricityMeter; + +class EvseChargePointHardyBarthEcb1ImplTest { + + /** Chargecontrol response for a stopped (no car) scenario. */ + private static final String CHARGECONTROL_NO_CAR = """ + { + "chargecontrol": { + "mode": "manual", + "connected": false, + "manualmodeamp": 10.0, + "stateid": 17, + "currentpwmamp": 0.0, + "state": "A", + "vendor": "Phoenix Contact", + "version": "V1.3.1" + }, + "protocol-version": "1.4" + } + """; + + /** Chargecontrol response for a car connected but charging paused. */ + private static final String CHARGECONTROL_CAR_PAUSED = """ + { + "chargecontrol": { + "mode": "manual", + "connected": true, + "manualmodeamp": 0.0, + "stateid": 17, + "currentpwmamp": 0.0, + "state": "B", + "vendor": "Phoenix Contact", + "version": "V1.3.1" + }, + "protocol-version": "1.4" + } + """; + + /** Chargecontrol response for active charging. */ + private static final String CHARGECONTROL_CHARGING = """ + { + "chargecontrol": { + "mode": "manual", + "connected": true, + "manualmodeamp": 16.0, + "stateid": 5, + "currentpwmamp": 16.0, + "state": "C", + "vendor": "Phoenix Contact", + "version": "V1.3.1" + }, + "protocol-version": "1.4" + } + """; + + /** Chargecontrol response that should be forced back to manual mode. */ + private static final String CHARGECONTROL_AUTOMATIC_MODE = """ + { + "chargecontrol": { + "mode": "automatic", + "connected": true, + "manualmodeamp": 10.0, + "stateid": 17, + "currentpwmamp": 0.0, + "state": "B", + "vendor": "Phoenix Contact", + "version": "V1.3.1" + }, + "protocol-version": "1.4" + } + """; + + /** Meter response with charging values. */ + private static final String METER_CHARGING = """ + { + "meter": { + "serial": 75740051, + "vendor": "eCHARGE", + "type": "eCB1 intern", + "name": "Carport", + "data": { + "1-0:1.4.0": 11040.0, + "1-0:1.8.0": 10000.0, + "1-0:21.4.0": 3680.0, + "1-0:41.4.0": 3680.0, + "1-0:61.4.0": 3680.0, + "1-0:31.4.0": 16.0, + "1-0:51.4.0": 16.0, + "1-0:71.4.0": 16.0, + "1-0:32.4.0": 230.0, + "1-0:52.4.0": 230.0, + "1-0:72.4.0": 230.0 + }, + "id": 1 + }, + "protocol-version": "1.4" + } + """; + + /** Meter response without readable data. */ + private static final String METER_WITHOUT_DATA = """ + { + "meter": { + "serial": 75740051, + "vendor": "eCHARGE", + "type": "eCB1 intern" + }, + "protocol-version": "1.4" + } + """; + + private static ComponentTest buildTest(EvseChargePointHardyBarthEcb1Impl sut) throws Exception { + return new ComponentTest(sut) // + .addReference("httpBridgeFactory", + ofBridgeImpl(DummyBridgeHttpFactory::dummyEndpointFetcher, + DummyBridgeHttpFactory::dummyBridgeHttpExecutor)) // + .addReference("httpBridgeCycleServiceDefinition", + new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // + .activate(MyConfig.create() // + .setId("evseChargePoint0") // + .setIp("192.168.2.8") // + .setChargeControlId(1) // + .setMeterId(1) // + .setMaxHwCurrent(32_000) // + .build()); + } + + @Test + void testNoCar() throws Exception { + var sut = new EvseChargePointHardyBarthEcb1Impl(); + var test = buildTest(sut); + var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); + test // + .next(new TestCase() // + .onBeforeProcessImage(() -> handler.handleChargeControlResponse(CHARGECONTROL_NO_CAR)) // + .output(EvseChargePoint.ChannelId.IS_READY_FOR_CHARGING, false) // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE, "A") // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_CONNECTED, false) // + ); + } + + @Test + void testCarPaused() throws Exception { + var sut = new EvseChargePointHardyBarthEcb1Impl(); + var test = buildTest(sut); + var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); + test // + .next(new TestCase() // + .onBeforeProcessImage(() -> handler.handleChargeControlResponse(CHARGECONTROL_CAR_PAUSED)) // + .output(EvseChargePoint.ChannelId.IS_READY_FOR_CHARGING, true) // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE, "B") // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_CONNECTED, true) // + ); + } + + @Test + void testCharging() throws Exception { + var sut = new EvseChargePointHardyBarthEcb1Impl(); + var test = buildTest(sut); + var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); + test // + .next(new TestCase() // + .activateStrictMode() // + .onBeforeProcessImage(() -> { + handler.handleChargeControlResponse(CHARGECONTROL_CHARGING); + handler.handleMeterResponse(METER_CHARGING); + }) // + .output(EvseChargePoint.ChannelId.IS_READY_FOR_CHARGING, true) // + + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 11040) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, 3680) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, 3680) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, 3680) // + + .output(ElectricityMeter.ChannelId.CURRENT, 48000) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, 16_000) // + .output(ElectricityMeter.ChannelId.CURRENT_L2, 16_000) // + .output(ElectricityMeter.ChannelId.CURRENT_L3, 16_000) // + + .output(ElectricityMeter.ChannelId.VOLTAGE, 230_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 230_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L2, 230_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L3, 230_000) // + + .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY, null) // + .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY_L1, null) // + .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY_L2, null) // + .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY_L3, null) // + .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, 10000L) // + .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY_L1, null) // + .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY_L2, null) // + .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY_L3, null) // + .output(ElectricityMeter.ChannelId.FREQUENCY, null) // + .output(ElectricityMeter.ChannelId.REACTIVE_POWER, null) // + .output(ElectricityMeter.ChannelId.REACTIVE_POWER_L1, null) // + .output(ElectricityMeter.ChannelId.REACTIVE_POWER_L2, null) // + .output(ElectricityMeter.ChannelId.REACTIVE_POWER_L3, null) // + + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_CONNECTED, true) // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_CURRENT_PWM_AMP, 16.0) // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_MANUAL_MODE_AMP, 16.0) // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_SERIAL, 75740051) // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_TYPE, "eCB1 intern") // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, "eCHARGE") // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_MODE, "manual") // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE, "C") // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE_ID, 5) // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_VENDOR, "Phoenix Contact") // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_VERSION, "V1.3.1") // + + .output(OpenemsComponent.ChannelId.STATE, Level.OK) // + ); + } + + @Test + void testInvalidMeterResponseClearsPreviousValues() throws Exception { + var sut = new EvseChargePointHardyBarthEcb1Impl(); + var test = buildTest(sut); + var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); + test // + .next(new TestCase() // + .onBeforeProcessImage(() -> handler.handleMeterResponse(METER_CHARGING)) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 11040) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, 16_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 230_000) // + .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, 10000L)) // + .next(new TestCase() // + .onBeforeProcessImage(() -> handler.handleMeterResponse(METER_WITHOUT_DATA)) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, null) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, null) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, null) // + .output(ElectricityMeter.ChannelId.CURRENT_L2, null) // + .output(ElectricityMeter.ChannelId.CURRENT_L3, null) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, null)); + } + + @Test + void testMalformedMeterResponseClearsPreviousValues() throws Exception { + var sut = new EvseChargePointHardyBarthEcb1Impl(); + var test = buildTest(sut); + var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); + test // + .next(new TestCase() // + .onBeforeProcessImage(() -> handler.handleMeterResponse(METER_CHARGING)) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 11040) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 230_000)) // + .next(new TestCase() // + .onBeforeProcessImage(() -> handler.handleMeterResponse("{")) // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_SERIAL, null) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, null) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, null)); + } + + @Test + void testMissingMeterClearsPreviousValues() throws Exception { + var sut = new EvseChargePointHardyBarthEcb1Impl(); + var test = buildTest(sut); + var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); + test // + .next(new TestCase() // + .onBeforeProcessImage(() -> handler.handleMeterResponse(METER_CHARGING)) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 11040) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, 16_000)) // + .next(new TestCase() // + .onBeforeProcessImage(() -> handler.handleMeterResponse("{}")) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, null) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, null)); + } + + @Test + void testSetCurrentSendsHttpRequests() throws Exception { + final var pool = DummyBridgeHttpFactory.dummyBridgeHttpExecutor(false); + final var httpBundle = DummyBridgeHttpBundle.of(pool); + final var sentUrls = new java.util.ArrayList(); + final var sentBodies = new java.util.ArrayList(); + + httpBundle.fetcher().addEndpointHandler(ep -> { + sentUrls.add(ep.url()); + if (ep.body() != null) { + sentBodies.add(ep.body()); + } + return HttpResponse.ok("ok"); + }); + + var sut = new EvseChargePointHardyBarthEcb1Impl(); + new ComponentTest(sut) // + .addReference("httpBridgeFactory", httpBundle.factory()) // + .addReference("httpBridgeCycleServiceDefinition", + new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // + .activate(MyConfig.create() // + .setId("evseChargePoint0") // + .setIp("192.168.2.8") // + .setChargeControlId(1) // + .setMeterId(1) // + .setMaxHwCurrent(32_000) // + .build()); + + // Simulate the controller calling apply() with 10 A + var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); + handler.setTarget(10); + pool.update(); + + assertTrue(sentUrls.stream().anyMatch(u -> u.contains("/mode/manual/ampere")), "Expected a manualmodeamp POST"); + assertTrue(sentUrls.stream().anyMatch(u -> u.contains("/start")), "Expected a start POST"); + assertTrue(sentBodies.stream().anyMatch(b -> b.contains("manualmodeamp=10")), "Expected manualmodeamp=10"); + } + + @Test + void testSetCurrentSkipsDuplicateTarget() throws Exception { + final var pool = DummyBridgeHttpFactory.dummyBridgeHttpExecutor(false); + final var httpBundle = DummyBridgeHttpBundle.of(pool); + final var sentUrls = new java.util.ArrayList(); + + httpBundle.fetcher().addEndpointHandler(ep -> { + sentUrls.add(ep.url()); + return HttpResponse.ok("ok"); + }); + + var sut = new EvseChargePointHardyBarthEcb1Impl(); + new ComponentTest(sut) // + .addReference("httpBridgeFactory", httpBundle.factory()) // + .addReference("httpBridgeCycleServiceDefinition", + new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // + .activate(MyConfig.create() // + .setId("evseChargePoint0") // + .setIp("192.168.2.8") // + .setChargeControlId(1) // + .setMeterId(1) // + .setMaxHwCurrent(32_000) // + .build()); + + var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); + handler.setTarget(10); + pool.update(); + handler.setTarget(10); + pool.update(); + + assertEquals(1, sentUrls.stream().filter(u -> u.contains("/mode/manual/ampere")).count()); + assertEquals(1, sentUrls.stream().filter(u -> u.contains("/start")).count()); + } + + @Test + void testAutomaticModeIsForcedBackToManual() throws Exception { + final var pool = DummyBridgeHttpFactory.dummyBridgeHttpExecutor(false); + final var httpBundle = DummyBridgeHttpBundle.of(pool); + final var sentBodies = new java.util.ArrayList(); + + httpBundle.fetcher().addEndpointHandler(ep -> { + if (ep.body() != null) { + sentBodies.add(ep.body()); + } + return HttpResponse.ok("ok"); + }); + + var sut = new EvseChargePointHardyBarthEcb1Impl(); + new ComponentTest(sut) // + .addReference("httpBridgeFactory", httpBundle.factory()) // + .addReference("httpBridgeCycleServiceDefinition", + new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // + .activate(MyConfig.create() // + .setId("evseChargePoint0") // + .setIp("192.168.2.8") // + .setChargeControlId(1) // + .setMeterId(1) // + .setMaxHwCurrent(32_000) // + .build()); + + sentBodies.clear(); + var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); + handler.handleChargeControlResponse(CHARGECONTROL_AUTOMATIC_MODE); + pool.update(); + + assertTrue(sentBodies.stream().anyMatch(b -> b.contains("mode=manual")), "Expected mode=manual"); + } + + @Test + void testGetChargePointAbilitiesDefaultThreePhase() throws Exception { + var sut = new EvseChargePointHardyBarthEcb1Impl(); + buildTest(sut); + + var abilities = sut.getChargePointAbilities(); + var ampere = assertInstanceOf(ApplySetPoint.Ability.Ampere.class, abilities.applySetPoint()); + assertEquals(Phase.SingleOrThreePhase.THREE_PHASE, ampere.phase()); + assertEquals(6, ampere.min()); + assertEquals(32, ampere.max()); + assertFalse(abilities.isEvConnected()); + } + + @Test + void testGetChargePointAbilitiesSinglePhase() throws Exception { + var sut = new EvseChargePointHardyBarthEcb1Impl(); + buildTest(sut); + withValue(sut, ElectricityMeter.ChannelId.CURRENT_L1, Evcs.MIN_EVCS_ACTIVITY_CURRENT + 1); + + var abilities = sut.getChargePointAbilities(); + var ampere = assertInstanceOf(ApplySetPoint.Ability.Ampere.class, abilities.applySetPoint()); + assertEquals(Phase.SingleOrThreePhase.SINGLE_PHASE, ampere.phase()); + } + + @Test + void testGetChargePointAbilitiesReadOnly() throws Exception { + var sut = new EvseChargePointHardyBarthEcb1Impl(); + new ComponentTest(sut) // + .addReference("httpBridgeFactory", + ofBridgeImpl(DummyBridgeHttpFactory::dummyEndpointFetcher, + DummyBridgeHttpFactory::dummyBridgeHttpExecutor)) // + .addReference("httpBridgeCycleServiceDefinition", + new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // + .activate(MyConfig.create() // + .setId("evseChargePoint0") // + .setIp("192.168.2.8") // + .setChargeControlId(1) // + .setMeterId(1) // + .setMaxHwCurrent(32_000) // + .setReadOnly(true) // + .build()); + + assertFalse(sut.getChargePointAbilities().applySetPoint() instanceof ApplySetPoint.Ability.Ampere); + } + + @Test + void testApplyCallsHandler() throws Exception { + final var pool = DummyBridgeHttpFactory.dummyBridgeHttpExecutor(false); + final var httpBundle = DummyBridgeHttpBundle.of(pool); + final var sentUrls = new java.util.ArrayList(); + + httpBundle.fetcher().addEndpointHandler(ep -> { + sentUrls.add(ep.url()); + return HttpResponse.ok("ok"); + }); + + var sut = new EvseChargePointHardyBarthEcb1Impl(); + new ComponentTest(sut) // + .addReference("httpBridgeFactory", httpBundle.factory()) // + .addReference("httpBridgeCycleServiceDefinition", + new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // + .activate(MyConfig.create() // + .setId("evseChargePoint0") // + .setIp("192.168.2.8") // + .setChargeControlId(1) // + .setMeterId(1) // + .setMaxHwCurrent(32_000) // + .build()); + + final var abilities = sut.getChargePointAbilities(); + sut.apply(ChargePointActions.from(abilities).setApplySetPointInAmpere(10).build()); + pool.update(); + + assertTrue(sentUrls.stream().anyMatch(u -> u.contains("/start")), "Expected a /start POST via apply()"); + } + + @Test + void testPauseStopsCharging() throws Exception { + final var pool = DummyBridgeHttpFactory.dummyBridgeHttpExecutor(false); + final var httpBundle = DummyBridgeHttpBundle.of(pool); + final var sentUrls = new java.util.ArrayList(); + + httpBundle.fetcher().addEndpointHandler(ep -> { + sentUrls.add(ep.url()); + return HttpResponse.ok("ok"); + }); + + var sut = new EvseChargePointHardyBarthEcb1Impl(); + new ComponentTest(sut) // + .addReference("httpBridgeFactory", httpBundle.factory()) // + .addReference("httpBridgeCycleServiceDefinition", + new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // + .activate(MyConfig.create() // + .setId("evseChargePoint0") // + .setIp("192.168.2.8") // + .setChargeControlId(1) // + .setMeterId(1) // + .setMaxHwCurrent(32_000) // + .build()); + + var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); + handler.setTarget(0); + pool.update(); + + assertTrue(sentUrls.stream().anyMatch(u -> u.contains("/stop")), "Expected a stop POST"); + assertFalse(sentUrls.stream().anyMatch(u -> u.contains("/start")), "Should not send /start when stopping"); + } +} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/ecb1/MyConfig.java b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/ecb1/MyConfig.java new file mode 100644 index 00000000000..56bed694bb2 --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/ecb1/MyConfig.java @@ -0,0 +1,106 @@ +package io.openems.edge.evse.chargepoint.hardybarth.ecb1; + +import io.openems.common.test.AbstractComponentConfig; +import io.openems.edge.meter.api.PhaseRotation; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + private String id; + private String ip; + private int chargeControlId; + private int meterId; + private int maxHwCurrent; + private PhaseRotation phaseRotation = PhaseRotation.L1_L2_L3; + private boolean readOnly = false; + + private Builder() { + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setIp(String ip) { + this.ip = ip; + return this; + } + + public Builder setChargeControlId(int chargeControlId) { + this.chargeControlId = chargeControlId; + return this; + } + + public Builder setMeterId(int meterId) { + this.meterId = meterId; + return this; + } + + public Builder setMaxHwCurrent(int maxHwCurrent) { + this.maxHwCurrent = maxHwCurrent; + return this; + } + + public Builder setPhaseRotation(PhaseRotation phaseRotation) { + this.phaseRotation = phaseRotation; + return this; + } + + public Builder setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + return this; + } + + public MyConfig build() { + return new MyConfig(this); + } + } + + /** + * Creates 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 ip() { + return this.builder.ip; + } + + @Override + public int chargeControlId() { + return this.builder.chargeControlId; + } + + @Override + public int meterId() { + return this.builder.meterId; + } + + @Override + public int maxHwCurrent() { + return this.builder.maxHwCurrent; + } + + @Override + public PhaseRotation phaseRotation() { + return this.builder.phaseRotation; + } + + @Override + public boolean readOnly() { + return this.builder.readOnly; + } +} diff --git a/io.openems.edge.evse.chargepoint.keba/src/io/openems/edge/evcs/keba/modbus/EvcsKebaModbusImpl.java b/io.openems.edge.evse.chargepoint.keba/src/io/openems/edge/evcs/keba/modbus/EvcsKebaModbusImpl.java index f719ecb3df1..632e1996508 100644 --- a/io.openems.edge.evse.chargepoint.keba/src/io/openems/edge/evcs/keba/modbus/EvcsKebaModbusImpl.java +++ b/io.openems.edge.evse.chargepoint.keba/src/io/openems/edge/evcs/keba/modbus/EvcsKebaModbusImpl.java @@ -2,7 +2,6 @@ import static io.openems.common.types.OpenemsType.INTEGER; import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.DIRECT_1_TO_1; -import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_1; import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_3; import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_MINUS_1; import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_MINUS_3; @@ -63,7 +62,6 @@ import io.openems.edge.evcs.api.Status; import io.openems.edge.evcs.api.WriteHandler; import io.openems.edge.evse.chargepoint.keba.common.EvcsKeba; -import io.openems.edge.evse.chargepoint.keba.common.EvseKeba; import io.openems.edge.evse.chargepoint.keba.common.Keba; import io.openems.edge.evse.chargepoint.keba.common.KebaModbus; import io.openems.edge.evse.chargepoint.keba.common.KebaUtils; diff --git a/io.openems.edge.evse.chargepoint.keba/test/io/openems/edge/evcs/keba/modbus/EvcsKebaModbusImplTest.java b/io.openems.edge.evse.chargepoint.keba/test/io/openems/edge/evcs/keba/modbus/EvcsKebaModbusImplTest.java index 491943666c9..42a4253d93e 100644 --- a/io.openems.edge.evse.chargepoint.keba/test/io/openems/edge/evcs/keba/modbus/EvcsKebaModbusImplTest.java +++ b/io.openems.edge.evse.chargepoint.keba/test/io/openems/edge/evcs/keba/modbus/EvcsKebaModbusImplTest.java @@ -7,7 +7,6 @@ import static io.openems.edge.evse.chargepoint.keba.common.CommonNaturesTest.testManagedEvcsChannels; import static io.openems.edge.evse.chargepoint.keba.common.EvcsKebaTest.testEvcsKebaChannels; import static io.openems.edge.evse.chargepoint.keba.common.KebaModbusTest.prepareKebaModbus; -import static io.openems.edge.evse.chargepoint.keba.common.KebaModbusTest.testEnergyLimitWriteScale; import static io.openems.edge.evse.chargepoint.keba.common.KebaModbusTest.testKebaModbusChannels; import static io.openems.edge.evse.chargepoint.keba.common.KebaTest.testKebaChannels; import static io.openems.edge.meter.api.PhaseRotation.L2_L3_L1;