From 2addfcfbabb6745d70f9ad8891c42c13e12270d0 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 2 Sep 2026 11:34:15 +0200 Subject: [PATCH 01/19] [Edge] Add Hardy Barth cPH1 EVCS support via ECB1 REST API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `Evcs.HardyBarth.cPH1` to the existing Hardy Barth bundle, supporting the eCharge ECB1 REST API used by the Hardy Barth cPH1 wallbox. The new component polls `/api/v1/chargecontrols/{id}` and `/api/v1/meters/{id}` for status and OBIS-coded meter data, and dispatches start/stop/current-set commands as form-encoded HTTP POST requests. - `Config.java` – OSGi metatype (ip, chargeControlId, meterId, min/maxHwCurrent) - `EvcsHardyBarthEcb1.java` – interface with ECB1-specific RAW_* channels - `Ecb1Handler.java` – HTTP polling and write dispatch via BridgeHttp - `EvcsHardyBarthEcb1Impl.java` – AbstractManagedEvcsComponent + ElectricityMeter - JUnit 5 tests with real device JSON fixtures and HTTP write assertions - `readme.adoc` updated to document both Salia and cPH1 families --- .../readme.adoc | 69 +++- .../edge/evcs/hardybarth/ecb1/Config.java | 36 ++ .../evcs/hardybarth/ecb1/Ecb1Handler.java | 355 ++++++++++++++++++ .../hardybarth/ecb1/EvcsHardyBarthEcb1.java | 89 +++++ .../ecb1/EvcsHardyBarthEcb1Impl.java | 155 ++++++++ .../ecb1/EvcsHardyBarthEcb1ImplTest.java | 351 +++++++++++++++++ .../edge/evcs/hardybarth/ecb1/MyConfig.java | 94 +++++ 7 files changed, 1145 insertions(+), 4 deletions(-) create mode 100644 io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Config.java create mode 100644 io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java create mode 100644 io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1.java create mode 100644 io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java create mode 100644 io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1ImplTest.java create mode 100644 io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/MyConfig.java diff --git a/io.openems.edge.evse.chargepoint.hardybarth/readme.adoc b/io.openems.edge.evse.chargepoint.hardybarth/readme.adoc index 35d271ed5b3..52cb421d3ac 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)** (`Evcs.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]] +=== <<_evcs_cph1,EVCS Hardy Barth cPH1>> + +*Name*: EVCS Hardy Barth cPH1 + +*Factory-PID*: `Evcs.HardyBarth.cPH1` + +.Implemented Natures/Interfaces +* EvcsHardyBarthEcb1 +* OpenemsComponent +* EventHandler +* Evcs +* ManagedEvcs +* ElectricityMeter + +*Description*: EVCS 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 +* `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) + +.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}`) +|=== + +[[_evcs_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/evcs/hardybarth/ecb1/Config.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Config.java new file mode 100644 index 00000000000..cc66836bd3b --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Config.java @@ -0,0 +1,36 @@ +package io.openems.edge.evcs.hardybarth.ecb1; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(// + name = "EVCS 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 "evcs0"; + + @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 = "Minimum hardware current", description = "Minimum current of the Charger in mA.", required = true) + int minHwCurrent() default 6000; + + @AttributeDefinition(name = "Maximum hardware current", description = "Maximum current of the Charger in mA.", required = true) + int maxHwCurrent() default 32000; + + String webconsole_configurationFactory_nameHint() default "EVCS Hardy Barth cPH1 [{id}]"; +} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java new file mode 100644 index 00000000000..36a4c0910e0 --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java @@ -0,0 +1,355 @@ +package io.openems.edge.evcs.hardybarth.ecb1; + +import static io.openems.common.bridge.http.api.HttpMethod.POST; +import static io.openems.edge.common.channel.ChannelUtils.setValue; +import static java.lang.Math.round; + +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; +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.HttpMethod; +import io.openems.common.function.BooleanConsumer; +import io.openems.common.types.OpenemsType; +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.evcs.api.Evcs; +import io.openems.edge.evcs.api.Status; +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 EvcsHardyBarthEcb1Impl 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(EvcsHardyBarthEcb1Impl parent, String ip, int chargeControlId, int meterId, + BridgeHttpFactory httpBridgeFactory, HttpBridgeCycleServiceDefinition cycleServiceDef, + BooleanConsumer communicationFailed) { + 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.response()); + communicationFailed.accept(false); + }, // + error -> { + setValue(this.parent, Evcs.ChannelId.CHARGINGSTATION_COMMUNICATION_FAILED, true); + communicationFailed.accept(true); + }); + + // Subscribe for meter data (every cycle) + this.cycleService.subscribeCycle(1, // + this.baseUrl + "/meters/" + meterId, // + response -> this.handleMeterResponse(response.response()), // + 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 (accepted for dispatch) + */ + public boolean setTarget(int currentA) { + if (currentA == this.lastTargetCurrentA) { + return true; + } + this.targetExecutor.submit(() -> { + if (currentA == 0) { + this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/stop") // + .setMethod(POST) // + .build()); + } else { + this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/mode/manual/ampere") // + .setMethod(POST) // + .setBodyFormEncoded(Map.of("manualmodeamp", String.valueOf(currentA))) // + .build()); + this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/start") // + .setMethod(POST) // + .build()); + } + this.lastTargetCurrentA = currentA; + }); + return true; + } + + /** + * 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 + */ + 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()); + return; + } + + var cc = json.getAsJsonObject("chargecontrol"); + if (cc == null) { + return; + } + + var stateId = getIntOrNull(cc, "stateid"); + var state = getStringOrNull(cc, "state"); + var mode = getStringOrNull(cc, "mode"); + var connected = getBooleanOrNull(cc, "connected"); + var manualModeAmp = getDoubleOrNull(cc, "manualmodeamp"); + var currentPwmAmp = getDoubleOrNull(cc, "currentpwmamp"); + var vendor = getStringOrNull(cc, "vendor"); + var version = getStringOrNull(cc, "version"); + + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_STATE_ID, stateId); + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_STATE, state); + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_MODE, mode); + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED, connected); + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_MANUAL_MODE_AMP, manualModeAmp); + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_CURRENT_PWM_AMP, currentPwmAmp); + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_VENDOR, vendor); + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_VERSION, version); + + // Re-set manual mode if the device has drifted to another mode + if (mode != null && !mode.equals("manual")) { + this.setManualMode(); + } + + this.parent._setChargingstationCommunicationFailed(false); + this.parent._setStatus(this.toStatus(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 + */ + void handleMeterResponse(String body) { + JsonObject json; + try { + json = JsonUtils.parseToJsonObject(body); + } catch (Exception e) { + this.log.warn("Cannot parse ECB1 meter response: " + e.getMessage()); + return; + } + + var meter = json.getAsJsonObject("meter"); + if (meter == null) { + return; + } + + var data = meter.getAsJsonObject("data"); + if (data == null) { + return; + } + + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_METER_SERIAL, getIntOrNull(meter, "serial")); + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, getStringOrNull(meter, "vendor")); + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_METER_TYPE, getStringOrNull(meter, "type")); + + // Active power (W) + var powerTotal = roundToInt(getObisDouble(data, OBIS_POWER_TOTAL)); + var powerL1 = roundToInt(getObisDouble(data, OBIS_POWER_L1)); + var powerL2 = roundToInt(getObisDouble(data, OBIS_POWER_L2)); + var powerL3 = roundToInt(getObisDouble(data, OBIS_POWER_L3)); + + this.parent._setActivePower(powerTotal); + this.parent._setActivePowerL1(powerL1); + this.parent._setActivePowerL2(powerL2); + this.parent._setActivePowerL3(powerL3); + + // Current (A → mA) + var currentL1 = roundToInt(scale(getObisDouble(data, OBIS_CURRENT_L1), 1000.0)); + var currentL2 = roundToInt(scale(getObisDouble(data, OBIS_CURRENT_L2), 1000.0)); + var currentL3 = roundToInt(scale(getObisDouble(data, OBIS_CURRENT_L3), 1000.0)); + + this.parent._setCurrentL1(currentL1); + this.parent._setCurrentL2(currentL2); + this.parent._setCurrentL3(currentL3); + + // Voltage (V → mV) + var voltageL1 = roundToInt(scale(getObisDouble(data, OBIS_VOLTAGE_L1), 1000.0)); + var voltageL2 = roundToInt(scale(getObisDouble(data, OBIS_VOLTAGE_L2), 1000.0)); + var voltageL3 = roundToInt(scale(getObisDouble(data, OBIS_VOLTAGE_L3), 1000.0)); + + this.parent._setVoltageL1(voltageL1); + this.parent._setVoltageL2(voltageL2); + this.parent._setVoltageL3(voltageL3); + + // Energy (Wh) + var energyWh = getObisDouble(data, OBIS_ENERGY_TOTAL); + Long energyWhLong = energyWh == null ? null : (long) Math.round(energyWh); + this.parent._setActiveProductionEnergy(energyWhLong); + this.parent._setActiveConsumptionEnergy(energyWhLong); + } + + // ------------------------------------------------------------------------- + // Status mapping + // ------------------------------------------------------------------------- + + private Status toStatus(String state, Integer stateId, Boolean connected) { + if (state == null || state.isEmpty()) { + return Status.UNDEFINED; + } + var firstChar = state.charAt(0); + return switch (firstChar) { + case 'A' -> Status.NOT_READY_FOR_CHARGING; + case 'B' -> { + // StateID 17 = explicitly paused by the controller + if (stateId != null && stateId == 17) { + yield Status.CHARGING_REJECTED; + } + yield Status.READY_FOR_CHARGING; + } + case 'C', 'D' -> Status.CHARGING; + case 'E', 'F' -> Status.ERROR; + default -> Status.UNDEFINED; + }; + } + + // ------------------------------------------------------------------------- + // JSON helpers + // ------------------------------------------------------------------------- + + private static Double getObisDouble(JsonObject data, String obisCode) { + var element = data.get(obisCode); + if (element == null || element.isJsonNull()) { + return null; + } + try { + return element.getAsDouble(); + } catch (Exception e) { + return null; + } + } + + private static Integer roundToInt(Double value) { + return value == null ? null : (int) round(value); + } + + private static Double scale(Double value, double factor) { + return value == null ? null : value * factor; + } + + private static Integer getIntOrNull(JsonObject obj, String key) { + var el = obj.get(key); + if (el == null || el.isJsonNull()) { + return null; + } + try { + return el.getAsInt(); + } catch (Exception e) { + return null; + } + } + + private static String getStringOrNull(JsonObject obj, String key) { + var el = obj.get(key); + if (el == null || el.isJsonNull()) { + return null; + } + return el.getAsString(); + } + + private static Boolean getBooleanOrNull(JsonObject obj, String key) { + var el = obj.get(key); + if (el == null || el.isJsonNull()) { + return null; + } + try { + return el.getAsBoolean(); + } catch (Exception e) { + return null; + } + } + + private static Double getDoubleOrNull(JsonObject obj, String key) { + var el = obj.get(key); + if (el == null || el.isJsonNull()) { + return null; + } + try { + return el.getAsDouble(); + } catch (Exception e) { + return null; + } + } +} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1.java new file mode 100644 index 00000000000..a2c98b2a611 --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1.java @@ -0,0 +1,89 @@ +package io.openems.edge.evcs.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.evcs.api.Evcs; +import io.openems.edge.evcs.api.ManagedEvcs; +import io.openems.edge.meter.api.ElectricityMeter; + +public interface EvcsHardyBarthEcb1 extends OpenemsComponent, Evcs, ManagedEvcs, ElectricityMeter { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + /** + * ECB1 state ID. + * + *

+ */ + RAW_STATE_ID(Doc.of(OpenemsType.INTEGER) // + .text("ECB1 state ID (5=charging, 17=stopped)")), + + /** + * ECB1 IEC 61851 charge-point state. + * + * + */ + 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/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java new file mode 100644 index 00000000000..9b39f335f23 --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java @@ -0,0 +1,155 @@ +package io.openems.edge.evcs.hardybarth.ecb1; + +import static io.openems.edge.evcs.api.ChargingType.AC; +import static io.openems.edge.evcs.api.Phases.THREE_PHASE; +import static java.lang.Math.round; + +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventHandler; +import org.osgi.service.event.propertytypes.EventTopics; +import org.osgi.service.metatype.annotations.Designate; + +import io.openems.common.bridge.http.api.BridgeHttpFactory; +import io.openems.common.exceptions.OpenemsException; +import io.openems.edge.bridge.http.cycle.HttpBridgeCycleServiceDefinition; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.event.EdgeEventConstants; +import io.openems.edge.evcs.api.AbstractManagedEvcsComponent; +import io.openems.edge.evcs.api.Evcs; +import io.openems.edge.evcs.api.EvcsPower; +import io.openems.edge.evcs.api.ManagedEvcs; +import io.openems.edge.meter.api.ElectricityMeter; +import io.openems.edge.meter.api.MeterType; +import io.openems.edge.meter.api.PhaseRotation; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Evcs.HardyBarth.cPH1", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +@EventTopics({ // + EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE, // + EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE // +}) +public class EvcsHardyBarthEcb1Impl extends AbstractManagedEvcsComponent + implements EvcsHardyBarthEcb1, OpenemsComponent, EventHandler, ManagedEvcs, Evcs, ElectricityMeter { + + @Reference + private BridgeHttpFactory httpBridgeFactory; + + @Reference + private HttpBridgeCycleServiceDefinition httpBridgeCycleServiceDefinition; + + @Reference + private EvcsPower evcsPower; + + private Config config; + private Ecb1Handler handler; + + public EvcsHardyBarthEcb1Impl() { + super(// + OpenemsComponent.ChannelId.values(), // + ElectricityMeter.ChannelId.values(), // + Evcs.ChannelId.values(), // + ManagedEvcs.ChannelId.values(), // + EvcsHardyBarthEcb1.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()); + + this._setChargingType(AC); + this._setFixedMinimumHardwarePower(round(config.minHwCurrent() / 1000f) * DEFAULT_VOLTAGE * THREE_PHASE.getValue()); + this._setFixedMaximumHardwarePower(round(config.maxHwCurrent() / 1000f) * DEFAULT_VOLTAGE * THREE_PHASE.getValue()); + this._setPowerPrecision(230); + this._setPhases(THREE_PHASE); + + this.handler = new Ecb1Handler(this, config.ip(), config.chargeControlId(), config.meterId(), + this.httpBridgeFactory, this.httpBridgeCycleServiceDefinition, + this::_setChargingstationCommunicationFailed); + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + if (this.handler != null) { + this.handler.deactivate(); + } + } + + @Override + public void handleEvent(Event event) { + if (!this.isEnabled()) { + return; + } + super.handleEvent(event); + } + + @Override + public boolean applyChargePowerLimit(int power) throws OpenemsException { + var phases = this.getPhasesAsInt(); + var currentA = (int) round(power / (float) phases / 230.0f); + var minA = this.config.minHwCurrent() / 1000; + var maxA = this.config.maxHwCurrent() / 1000; + currentA = Math.max(minA, Math.min(currentA, maxA)); + return this.handler.setTarget(currentA); + } + + @Override + public boolean pauseChargeProcess() throws OpenemsException { + return this.handler.setTarget(0); + } + + @Override + public boolean applyDisplayText(String text) throws OpenemsException { + return false; + } + + @Override + public int getMinimumTimeTillChargingLimitTaken() { + return 30; + } + + @Override + public int getConfiguredMinimumHardwarePower() { + return round(this.config.minHwCurrent() / 1000f) * DEFAULT_VOLTAGE * THREE_PHASE.getValue(); + } + + @Override + public int getConfiguredMaximumHardwarePower() { + return round(this.config.maxHwCurrent() / 1000f) * DEFAULT_VOLTAGE * THREE_PHASE.getValue(); + } + + @Override + public EvcsPower getEvcsPower() { + return this.evcsPower; + } + + @Override + public boolean getConfiguredDebugMode() { + return false; + } + + @Override + public MeterType getMeterType() { + return MeterType.MANAGED_CONSUMPTION_METERED; + } + + @Override + public PhaseRotation getPhaseRotation() { + return PhaseRotation.L1_L2_L3; + } +} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1ImplTest.java b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1ImplTest.java new file mode 100644 index 00000000000..d51515b8cbd --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1ImplTest.java @@ -0,0 +1,351 @@ +package io.openems.edge.evcs.hardybarth.ecb1; + +import static io.openems.common.bridge.http.dummy.DummyBridgeHttpFactory.ofBridgeImpl; +import static io.openems.edge.evcs.api.Phases.THREE_PHASE; +import static io.openems.edge.evcs.api.Status.CHARGING; +import static io.openems.edge.evcs.api.Status.NOT_READY_FOR_CHARGING; +import static org.junit.jupiter.api.Assertions.assertFalse; +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.utils.ReflectionUtils; +import io.openems.edge.bridge.http.cycle.HttpBridgeCycleServiceDefinition; +import io.openems.edge.bridge.http.cycle.dummy.DummyCycleSubscriber; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.evcs.api.ChargingType; +import io.openems.edge.evcs.api.Evcs; +import io.openems.edge.evcs.api.Status; +import io.openems.edge.meter.api.ElectricityMeter; + +class EvcsHardyBarthEcb1ImplTest { + + /** Chargecontrol response for a stopped (no car) scenario – from a real device. */ + private static final String CHARGECONTROL_NO_CAR = """ + { + "chargecontrol": { + "modeid": 3, + "evminamp": 6, + "vendor": "Phoenix Contact", + "name": "evcc1", + "mode": "manual", + "type": "EVCC Basic (RS485)", + "id": 1, + "supplylinemaxamp": 32, + "connected": false, + "manualmodeamp": 10.0, + "stateid": 17, + "version": "V1.3.1", + "busid": 1, + "currentpwmamp": 0.0, + "state": "A\\u2019" + }, + "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" + } + """; + + /** Meter response from a real device (standby values). */ + private static final String METER_RESPONSE = """ + { + "meter": { + "serial": 75740051, + "vendor": "eCHARGE", + "type": "eCB1 intern", + "name": "Carport", + "data": { + "1-0:1.4.0": 3.5, + "1-0:1.8.0": 6541.3684, + "1-0:21.4.0": 3.5, + "1-0:41.4.0": 0.0, + "1-0:61.4.0": 0.0, + "1-0:31.4.0": 0.018, + "1-0:51.4.0": 0.0, + "1-0:71.4.0": 0.0, + "1-0:32.4.0": 228.373, + "1-0:52.4.0": 229.971, + "1-0:72.4.0": 228.620 + }, + "id": 1, + "ipaddress": "127.0.0.1", + "function": "socket" + }, + "protocol-version": "1.4" + } + """; + + /** Meter response for a charging scenario with clean numbers. */ + 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, + "ipaddress": "127.0.0.1", + "function": "socket" + }, + "protocol-version": "1.4" + } + """; + + @Test + void testNoCar() throws Exception { + var sut = new EvcsHardyBarthEcb1Impl(); + var test = new ComponentTest(sut) // + .addReference("httpBridgeFactory", + ofBridgeImpl(DummyBridgeHttpFactory::dummyEndpointFetcher, + DummyBridgeHttpFactory::dummyBridgeHttpExecutor)) // + .addReference("httpBridgeCycleServiceDefinition", + new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // + .activate(MyConfig.create() // + .setId("evcs0") // + .setIp("192.168.2.8") // + .setChargeControlId(1) // + .setMeterId(1) // + .setMinHwCurrent(6_000) // + .setMaxHwCurrent(32_000) // + .build()); + var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); + test // + .next(new TestCase() // + .onBeforeProcessImage(() -> { + handler.handleChargeControlResponse(CHARGECONTROL_NO_CAR); + handler.handleMeterResponse(METER_RESPONSE); + }) // + .output(Evcs.ChannelId.STATUS, NOT_READY_FOR_CHARGING) // + .output(Evcs.ChannelId.CHARGINGSTATION_COMMUNICATION_FAILED, false) // + .output(Evcs.ChannelId.CHARGING_TYPE, ChargingType.AC) // + .output(Evcs.ChannelId.PHASES, THREE_PHASE) // + .output(Evcs.ChannelId.FIXED_MINIMUM_HARDWARE_POWER, 4140) // + .output(Evcs.ChannelId.FIXED_MAXIMUM_HARDWARE_POWER, 22080) // + + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 4) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, 4) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, 0) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, 0) // + .output(ElectricityMeter.ChannelId.CURRENT, 18) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, 18) // + .output(ElectricityMeter.ChannelId.CURRENT_L2, 0) // + .output(ElectricityMeter.ChannelId.CURRENT_L3, 0) // + .output(ElectricityMeter.ChannelId.VOLTAGE, 228_988) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 228_373) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L2, 229_971) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L3, 228_620) // + .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY, 6541L) // + .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, 6541L) // + + .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE_ID, 17) // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE, "A’") // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_MODE, "manual") // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED, false) // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_MANUAL_MODE_AMP, 10.0) // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_CURRENT_PWM_AMP, 0.0) // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_VENDOR, "Phoenix Contact") // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_VERSION, "V1.3.1") // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_METER_SERIAL, 75740051) // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, "eCHARGE") // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_METER_TYPE, "eCB1 intern") // + ); + } + + @Test + void testCharging() throws Exception { + var sut = new EvcsHardyBarthEcb1Impl(); + var test = new ComponentTest(sut) // + .addReference("httpBridgeFactory", + ofBridgeImpl(DummyBridgeHttpFactory::dummyEndpointFetcher, + DummyBridgeHttpFactory::dummyBridgeHttpExecutor)) // + .addReference("httpBridgeCycleServiceDefinition", + new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // + .activate(MyConfig.create() // + .setId("evcs0") // + .setIp("192.168.2.8") // + .setChargeControlId(1) // + .setMeterId(1) // + .setMinHwCurrent(6_000) // + .setMaxHwCurrent(32_000) // + .build()); + var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); + test // + .next(new TestCase() // + .onBeforeProcessImage(() -> { + handler.handleChargeControlResponse(CHARGECONTROL_CHARGING); + handler.handleMeterResponse(METER_CHARGING); + }) // + .output(Evcs.ChannelId.STATUS, CHARGING) // + .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_L1, 16_000) // + .output(ElectricityMeter.ChannelId.CURRENT_L2, 16_000) // + .output(ElectricityMeter.ChannelId.CURRENT_L3, 16_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, 10000L) // + + .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE_ID, 5) // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE, "C") // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED, true) // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_MANUAL_MODE_AMP, 16.0) // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_CURRENT_PWM_AMP, 16.0) // + ); + } + + @Test + void testCarConnectedPaused() throws Exception { + var sut = new EvcsHardyBarthEcb1Impl(); + var test = new ComponentTest(sut) // + .addReference("httpBridgeFactory", + ofBridgeImpl(DummyBridgeHttpFactory::dummyEndpointFetcher, + DummyBridgeHttpFactory::dummyBridgeHttpExecutor)) // + .addReference("httpBridgeCycleServiceDefinition", + new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // + .activate(MyConfig.create() // + .setId("evcs0") // + .setIp("192.168.2.8") // + .setChargeControlId(1) // + .setMeterId(1) // + .setMinHwCurrent(6_000) // + .setMaxHwCurrent(32_000) // + .build()); + var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); + test // + .next(new TestCase() // + .onBeforeProcessImage(() -> handler.handleChargeControlResponse(CHARGECONTROL_CAR_PAUSED)) // + .output(Evcs.ChannelId.STATUS, Status.CHARGING_REJECTED) // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE, "B") // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED, true) // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE_ID, 17) // + ); + } + + @Test + void testSetCurrentSendsHttpRequests() throws Exception { + final var pool = DummyBridgeHttpFactory.dummyBridgeHttpExecutor(false); + final var httpBundle = DummyBridgeHttpBundle.of(pool); + final var sentBodies = new java.util.ArrayList(); + final var sentUrls = 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 EvcsHardyBarthEcb1Impl(); + new ComponentTest(sut) // + .addReference("httpBridgeFactory", httpBundle.factory()) // + .addReference("httpBridgeCycleServiceDefinition", + new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // + .activate(MyConfig.create() // + .setId("evcs0") // + .setIp("192.168.2.8") // + .setChargeControlId(1) // + .setMeterId(1) // + .setMinHwCurrent(6_000) // + .setMaxHwCurrent(32_000) // + .build()); + + // Trigger applyChargePowerLimit at 6900 W (3 phases × 10 A × 230 V) + sut.applyChargePowerLimit(6900); + + // Flush the executor so requests are actually dispatched + pool.update(); + + // Expect set-current and start requests + 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 (6900 W / 3 / 230 = 10 A)"); + } + + @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 EvcsHardyBarthEcb1Impl(); + new ComponentTest(sut) // + .addReference("httpBridgeFactory", httpBundle.factory()) // + .addReference("httpBridgeCycleServiceDefinition", + new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // + .activate(MyConfig.create() // + .setId("evcs0") // + .setIp("192.168.2.8") // + .setChargeControlId(1) // + .setMeterId(1) // + .setMinHwCurrent(6_000) // + .setMaxHwCurrent(32_000) // + .build()); + + sut.pauseChargeProcess(); + pool.update(); + + assertTrue(sentUrls.stream().anyMatch(u -> u.contains("/stop")), + "Expected a stop POST"); + } +} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/MyConfig.java b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/MyConfig.java new file mode 100644 index 00000000000..7f4e609327e --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/MyConfig.java @@ -0,0 +1,94 @@ +package io.openems.edge.evcs.hardybarth.ecb1; + +import io.openems.common.test.AbstractComponentConfig; + +@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 minHwCurrent; + private int maxHwCurrent; + + 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 setMinHwCurrent(int minHwCurrent) { + this.minHwCurrent = minHwCurrent; + return this; + } + + public Builder setMaxHwCurrent(int maxHwCurrent) { + this.maxHwCurrent = maxHwCurrent; + 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 minHwCurrent() { + return this.builder.minHwCurrent; + } + + @Override + public int maxHwCurrent() { + return this.builder.maxHwCurrent; + } +} From fc1a748fc0e8c8d6adf5245c7c3c842ff8cbb1ea Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 2 Sep 2026 11:58:22 +0200 Subject: [PATCH 02/19] [Edge] Fix Hardy Barth cPH1: send empty body on /start and /stop The ECB1 device returns 411 Length Required when POST requests have no Content-Length header. Added setBodyFormEncoded(Map.of()) for the start and stop endpoints so the HTTP client sets Content-Length: 0. Verified against real device at 192.168.2.8. --- .../src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java index 36a4c0910e0..9161b5ee1a7 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java @@ -113,6 +113,7 @@ public boolean setTarget(int currentA) { if (currentA == 0) { this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/stop") // .setMethod(POST) // + .setBodyFormEncoded(Map.of()) // .build()); } else { this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/mode/manual/ampere") // @@ -121,6 +122,7 @@ public boolean setTarget(int currentA) { .build()); this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/start") // .setMethod(POST) // + .setBodyFormEncoded(Map.of()) // .build()); } this.lastTargetCurrentA = currentA; From cb22a274aa660546578e4c9ba8478f10b9f95ec1 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 2 Sep 2026 14:12:45 +0200 Subject: [PATCH 03/19] [Edge] Fix Hardy Barth cPH1 ECB1: correct API usages causing CI build failure - Use HttpResponse.data() instead of non-existent .response() - Fix MeterType import: io.openems.common.types, not edge.meter.api - Replace LatestWinsFutureExecutor.submit(Runnable) with execute(Supplier, BiConsumer); chain start/mode requests via thenCompose --- .../evcs/hardybarth/ecb1/Ecb1Handler.java | 56 +++++++++++-------- .../ecb1/EvcsHardyBarthEcb1Impl.java | 2 +- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java index 9161b5ee1a7..208a98b9902 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java @@ -5,18 +5,18 @@ import static java.lang.Math.round; 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.JsonElement; 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.HttpMethod; +import io.openems.common.bridge.http.api.HttpResponse; import io.openems.common.function.BooleanConsumer; -import io.openems.common.types.OpenemsType; import io.openems.common.utils.JsonUtils; import io.openems.common.utils.LatestWinsFutureExecutor; import io.openems.edge.bridge.http.cycle.HttpBridgeCycleService; @@ -73,7 +73,7 @@ public Ecb1Handler(EvcsHardyBarthEcb1Impl parent, String ip, int chargeControlId this.cycleService.subscribeCycle(1, // this.baseUrl + "/chargecontrols/" + chargeControlId, // response -> { - this.handleChargeControlResponse(response.response()); + this.handleChargeControlResponse(response.data()); communicationFailed.accept(false); }, // error -> { @@ -84,7 +84,7 @@ public Ecb1Handler(EvcsHardyBarthEcb1Impl parent, String ip, int chargeControlId // Subscribe for meter data (every cycle) this.cycleService.subscribeCycle(1, // this.baseUrl + "/meters/" + meterId, // - response -> this.handleMeterResponse(response.response()), // + response -> this.handleMeterResponse(response.data()), // error -> this.log.warn("Failed to read ECB1 meter: " + error)); // Set manual mode on startup @@ -103,31 +103,41 @@ public void deactivate() { * Sets the charge target current. Call with 0 to stop charging. * * @param currentA target current in Ampere (0 = stop) - * @return true (accepted for dispatch) + * @return true if accepted for dispatch; false if the executor was cancelled */ public boolean setTarget(int currentA) { if (currentA == this.lastTargetCurrentA) { return true; } - this.targetExecutor.submit(() -> { - if (currentA == 0) { - this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/stop") // - .setMethod(POST) // - .setBodyFormEncoded(Map.of()) // - .build()); - } else { - this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/mode/manual/ampere") // - .setMethod(POST) // - .setBodyFormEncoded(Map.of("manualmodeamp", String.valueOf(currentA))) // - .build()); - this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/start") // + 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()); + } + return this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/mode/manual/ampere") // + .setMethod(POST) // + .setBodyFormEncoded(Map.of("manualmodeamp", String.valueOf(currentA))) // + .build()) // + .thenCompose(r -> this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/start") // .setMethod(POST) // .setBodyFormEncoded(Map.of()) // - .build()); - } - this.lastTargetCurrentA = currentA; - }); - return true; + .build())); } /** diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java index 9b39f335f23..507e1a3e427 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java @@ -24,8 +24,8 @@ import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.EvcsPower; import io.openems.edge.evcs.api.ManagedEvcs; +import io.openems.common.types.MeterType; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; import io.openems.edge.meter.api.PhaseRotation; @Designate(ocd = Config.class, factory = true) From 2f1a2eb1c182c0757918763323ea09a539d0d606 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 2 Sep 2026 14:46:34 +0200 Subject: [PATCH 04/19] [Edge] Fix Hardy Barth cPH1 ECB1: fix Checkstyle VariableDeclarationUsageDistance Inline single-use variables (manualModeAmp, currentPwmAmp, vendor, version) directly into setValue calls so no variable exceeds the 3-statement distance threshold. --- .../edge/evcs/hardybarth/ecb1/Ecb1Handler.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java index 208a98b9902..3ec0b5cf835 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java @@ -181,19 +181,14 @@ void handleChargeControlResponse(String body) { var state = getStringOrNull(cc, "state"); var mode = getStringOrNull(cc, "mode"); var connected = getBooleanOrNull(cc, "connected"); - var manualModeAmp = getDoubleOrNull(cc, "manualmodeamp"); - var currentPwmAmp = getDoubleOrNull(cc, "currentpwmamp"); - var vendor = getStringOrNull(cc, "vendor"); - var version = getStringOrNull(cc, "version"); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_STATE_ID, stateId); setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_STATE, state); setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_MODE, mode); setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED, connected); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_MANUAL_MODE_AMP, manualModeAmp); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_CURRENT_PWM_AMP, currentPwmAmp); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_VENDOR, vendor); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_VERSION, version); + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_MANUAL_MODE_AMP, getDoubleOrNull(cc, "manualmodeamp")); + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_CURRENT_PWM_AMP, getDoubleOrNull(cc, "currentpwmamp")); + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_VENDOR, getStringOrNull(cc, "vendor")); + setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_VERSION, getStringOrNull(cc, "version")); // Re-set manual mode if the device has drifted to another mode if (mode != null && !mode.equals("manual")) { From 4f38860603056267e301696b0db7d8b7512f56b3 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 2 Sep 2026 14:59:57 +0200 Subject: [PATCH 05/19] [Edge] Fix Hardy Barth cPH1 ECB1: fix Checkstyle distance for connected Interleave variable declarations with their setValue calls so each variable is used in the immediately following statement (distance = 1). --- .../io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java index 3ec0b5cf835..ecc7dba9821 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java @@ -178,12 +178,12 @@ void handleChargeControlResponse(String body) { } var stateId = getIntOrNull(cc, "stateid"); - var state = getStringOrNull(cc, "state"); - var mode = getStringOrNull(cc, "mode"); - var connected = getBooleanOrNull(cc, "connected"); setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_STATE_ID, stateId); + var state = getStringOrNull(cc, "state"); setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_STATE, state); + var mode = getStringOrNull(cc, "mode"); setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_MODE, mode); + var connected = getBooleanOrNull(cc, "connected"); setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED, connected); setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_MANUAL_MODE_AMP, getDoubleOrNull(cc, "manualmodeamp")); setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_CURRENT_PWM_AMP, getDoubleOrNull(cc, "currentpwmamp")); From 88bc48bb45f18d7ba9e9c6e7f593563516fc933a Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 2 Sep 2026 15:28:15 +0200 Subject: [PATCH 06/19] [Edge] Fix Hardy Barth cPH1: dispatch /mode/manual/ampere and /start in same cycle Both HTTP requests are now queued immediately so a single executor update flushes them together, matching the test expectation. --- .../edge/evcs/hardybarth/ecb1/Ecb1Handler.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java index ecc7dba9821..0575acbe51d 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java @@ -130,14 +130,14 @@ private CompletableFuture> dispatchTarget(int currentA) { .setBodyFormEncoded(Map.of()) // .build()); } - return this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/mode/manual/ampere") // + this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/mode/manual/ampere") // .setMethod(POST) // .setBodyFormEncoded(Map.of("manualmodeamp", String.valueOf(currentA))) // - .build()) // - .thenCompose(r -> this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/start") // - .setMethod(POST) // - .setBodyFormEncoded(Map.of()) // - .build())); + .build()); + return this.httpBridge.request(BridgeHttp.create(this.chargeControlUrl() + "/start") // + .setMethod(POST) // + .setBodyFormEncoded(Map.of()) // + .build()); } /** From 77442af6e9b452b0e23b66902e2f6fb93fde10fb Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 2 Sep 2026 19:28:55 +0200 Subject: [PATCH 07/19] [Edge] Add Hardy Barth cPH1 EVSE implementation (Evse.ChargePoint.HardyBarth.cPH1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract Ecb1Parent interface so Ecb1Handler works with both EVCS and EVSE - Add EvseChargePointHardyBarthEcb1Impl (Evse.ChargePoint.HardyBarth.cPH1) implementing EvseChargePoint + ElectricityMeter via the shared Ecb1Handler - Both components expose raw ECB1 diagnostic channels (RAW_STATE, RAW_CONNECTED, …) - Move toStatus() logic into EvcsHardyBarthEcb1Impl.onChargeControlStatus() --- .../evcs/hardybarth/ecb1/Ecb1Handler.java | 56 +--- .../edge/evcs/hardybarth/ecb1/Ecb1Parent.java | 27 ++ .../ecb1/EvcsHardyBarthEcb1Impl.java | 32 ++- .../chargepoint/hardybarth/ecb1/Config.java | 44 ++++ .../ecb1/EvseChargePointHardyBarthEcb1.java | 8 + .../EvseChargePointHardyBarthEcb1Impl.java | 129 ++++++++++ ...EvseChargePointHardyBarthEcb1ImplTest.java | 243 ++++++++++++++++++ .../chargepoint/hardybarth/ecb1/MyConfig.java | 112 ++++++++ 8 files changed, 602 insertions(+), 49 deletions(-) create mode 100644 io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Parent.java create mode 100644 io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Config.java create mode 100644 io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1.java create mode 100644 io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1Impl.java create mode 100644 io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1ImplTest.java create mode 100644 io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/ecb1/MyConfig.java diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java index 0575acbe51d..8e1f588b688 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java @@ -16,21 +16,17 @@ 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.function.BooleanConsumer; 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.evcs.api.Evcs; -import io.openems.edge.evcs.api.Status; -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. + * Reads charge-control status and meter data every cycle and dispatches write + * commands (start / stop / set current) on demand. */ public class Ecb1Handler { @@ -48,7 +44,7 @@ public class Ecb1Handler { private final Logger log = LoggerFactory.getLogger(Ecb1Handler.class); - private final EvcsHardyBarthEcb1Impl parent; + private final Ecb1Parent parent; private final String baseUrl; private final int chargeControlId; private final BridgeHttpFactory httpBridgeFactory; @@ -59,9 +55,8 @@ public class Ecb1Handler { /** Last target current sent to the device (A), or -1 if unknown. */ private int lastTargetCurrentA = -1; - public Ecb1Handler(EvcsHardyBarthEcb1Impl parent, String ip, int chargeControlId, int meterId, - BridgeHttpFactory httpBridgeFactory, HttpBridgeCycleServiceDefinition cycleServiceDef, - BooleanConsumer communicationFailed) { + 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; @@ -72,14 +67,8 @@ public Ecb1Handler(EvcsHardyBarthEcb1Impl parent, String ip, int chargeControlId // Subscribe for charge-control status (every cycle) this.cycleService.subscribeCycle(1, // this.baseUrl + "/chargecontrols/" + chargeControlId, // - response -> { - this.handleChargeControlResponse(response.data()); - communicationFailed.accept(false); - }, // - error -> { - setValue(this.parent, Evcs.ChannelId.CHARGINGSTATION_COMMUNICATION_FAILED, true); - communicationFailed.accept(true); - }); + response -> this.handleChargeControlResponse(response.data()), // + error -> this.parent.onCommunicationFailed(true)); // Subscribe for meter data (every cycle) this.cycleService.subscribeCycle(1, // @@ -195,12 +184,13 @@ void handleChargeControlResponse(String body) { this.setManualMode(); } - this.parent._setChargingstationCommunicationFailed(false); - this.parent._setStatus(this.toStatus(state, stateId, connected)); + this.parent.onCommunicationFailed(false); + this.parent.onChargeControlStatus(state, stateId, connected); } /** - * Parses a GET /api/v1/meters/{id} response and updates ElectricityMeter channels. + * 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, @@ -268,30 +258,6 @@ void handleMeterResponse(String body) { this.parent._setActiveConsumptionEnergy(energyWhLong); } - // ------------------------------------------------------------------------- - // Status mapping - // ------------------------------------------------------------------------- - - private Status toStatus(String state, Integer stateId, Boolean connected) { - if (state == null || state.isEmpty()) { - return Status.UNDEFINED; - } - var firstChar = state.charAt(0); - return switch (firstChar) { - case 'A' -> Status.NOT_READY_FOR_CHARGING; - case 'B' -> { - // StateID 17 = explicitly paused by the controller - if (stateId != null && stateId == 17) { - yield Status.CHARGING_REJECTED; - } - yield Status.READY_FOR_CHARGING; - } - case 'C', 'D' -> Status.CHARGING; - case 'E', 'F' -> Status.ERROR; - default -> Status.UNDEFINED; - }; - } - // ------------------------------------------------------------------------- // JSON helpers // ------------------------------------------------------------------------- diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Parent.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Parent.java new file mode 100644 index 00000000000..be1b3745f3c --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Parent.java @@ -0,0 +1,27 @@ +package io.openems.edge.evcs.hardybarth.ecb1; + +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.meter.api.ElectricityMeter; + +/** + * Common parent interface for components driven by the Hardy Barth ECB1 REST + * API. Implemented by both the legacy EVCS and the new EVSE component. + */ +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/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java index 507e1a3e427..daad403a7b5 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java @@ -17,6 +17,7 @@ import io.openems.common.bridge.http.api.BridgeHttpFactory; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.MeterType; import io.openems.edge.bridge.http.cycle.HttpBridgeCycleServiceDefinition; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; @@ -24,7 +25,7 @@ import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.EvcsPower; import io.openems.edge.evcs.api.ManagedEvcs; -import io.openems.common.types.MeterType; +import io.openems.edge.evcs.api.Status; import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.meter.api.PhaseRotation; @@ -39,7 +40,8 @@ EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE // }) public class EvcsHardyBarthEcb1Impl extends AbstractManagedEvcsComponent - implements EvcsHardyBarthEcb1, OpenemsComponent, EventHandler, ManagedEvcs, Evcs, ElectricityMeter { + implements EvcsHardyBarthEcb1, Ecb1Parent, OpenemsComponent, EventHandler, ManagedEvcs, Evcs, + ElectricityMeter { @Reference private BridgeHttpFactory httpBridgeFactory; @@ -77,8 +79,7 @@ private void activate(ComponentContext context, Config config) { this._setPhases(THREE_PHASE); this.handler = new Ecb1Handler(this, config.ip(), config.chargeControlId(), config.meterId(), - this.httpBridgeFactory, this.httpBridgeCycleServiceDefinition, - this::_setChargingstationCommunicationFailed); + this.httpBridgeFactory, this.httpBridgeCycleServiceDefinition); } @Override @@ -98,6 +99,29 @@ public void handleEvent(Event event) { super.handleEvent(event); } + @Override + public void onChargeControlStatus(String state, Integer stateId, Boolean connected) { + this._setStatus(toStatus(state, stateId)); + } + + @Override + public void onCommunicationFailed(boolean failed) { + this._setChargingstationCommunicationFailed(failed); + } + + private static Status toStatus(String state, Integer stateId) { + if (state == null || state.isEmpty()) { + return Status.UNDEFINED; + } + return switch (state.charAt(0)) { + case 'A' -> Status.NOT_READY_FOR_CHARGING; + case 'B' -> stateId != null && stateId == 17 ? Status.CHARGING_REJECTED : Status.READY_FOR_CHARGING; + case 'C', 'D' -> Status.CHARGING; + case 'E', 'F' -> Status.ERROR; + default -> Status.UNDEFINED; + }; + } + @Override public boolean applyChargePowerLimit(int power) throws OpenemsException { var phases = this.getPhasesAsInt(); 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..028e911dbd7 --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Config.java @@ -0,0 +1,44 @@ +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 = "Minimum hardware current", description = "Minimum current of the Charger in mA.", required = true) + int minHwCurrent() default 6000; + + @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/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..d2f1310fee4 --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1.java @@ -0,0 +1,8 @@ +package io.openems.edge.evse.chargepoint.hardybarth.ecb1; + +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 { +} 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..b40a768554a --- /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.evcs.hardybarth.ecb1.Ecb1Handler; +import io.openems.edge.evcs.hardybarth.ecb1.Ecb1Parent; +import io.openems.edge.evcs.hardybarth.ecb1.EvcsHardyBarthEcb1; +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 { + + @Reference + private BridgeHttpFactory httpBridgeFactory; + + @Reference + private HttpBridgeCycleServiceDefinition httpBridgeCycleServiceDefinition; + + private Config config; + private Ecb1Handler handler; + + public EvseChargePointHardyBarthEcb1Impl() { + super(// + OpenemsComponent.ChannelId.values(), // + ElectricityMeter.ChannelId.values(), // + EvseChargePoint.ChannelId.values(), // + EvcsHardyBarthEcb1.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'); + 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 connected = this.channel(EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED).value().orElse(false); + 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, // + this.config.minHwCurrent() / 1000, // + this.config.maxHwCurrent() / 1000)) // + .setIsEvConnected(connected) // + .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/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..1a11cd77a78 --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1ImplTest.java @@ -0,0 +1,243 @@ +package io.openems.edge.evse.chargepoint.hardybarth.ecb1; + +import static io.openems.common.bridge.http.dummy.DummyBridgeHttpFactory.ofBridgeImpl; +import static org.junit.jupiter.api.Assertions.assertFalse; +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.utils.ReflectionUtils; +import io.openems.edge.bridge.http.cycle.HttpBridgeCycleServiceDefinition; +import io.openems.edge.bridge.http.cycle.dummy.DummyCycleSubscriber; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.evcs.hardybarth.ecb1.Ecb1Handler; +import io.openems.edge.evcs.hardybarth.ecb1.EvcsHardyBarthEcb1; +import io.openems.edge.evse.api.chargepoint.EvseChargePoint; +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" + } + """; + + /** 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" + } + """; + + 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) // + .setMinHwCurrent(6_000) // + .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(EvcsHardyBarthEcb1.ChannelId.RAW_STATE, "A") // + .output(EvcsHardyBarthEcb1.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(EvcsHardyBarthEcb1.ChannelId.RAW_STATE, "B") // + .output(EvcsHardyBarthEcb1.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() // + .onBeforeProcessImage(() -> { + handler.handleChargeControlResponse(CHARGECONTROL_CHARGING); + handler.handleMeterResponse(METER_CHARGING); + }) // + .output(EvseChargePoint.ChannelId.IS_READY_FOR_CHARGING, true) // + .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE, "C") // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 11040) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, 16_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 230_000) // + .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY, 10000L) // + ); + } + + @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) // + .setMinHwCurrent(6_000) // + .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 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) // + .setMinHwCurrent(6_000) // + .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..f805666c89f --- /dev/null +++ b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/ecb1/MyConfig.java @@ -0,0 +1,112 @@ +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 minHwCurrent; + 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 setMinHwCurrent(int minHwCurrent) { + this.minHwCurrent = minHwCurrent; + 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); + } + } + + 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 minHwCurrent() { + return this.builder.minHwCurrent; + } + + @Override + public int maxHwCurrent() { + return this.builder.maxHwCurrent; + } + + @Override + public PhaseRotation phaseRotation() { + return this.builder.phaseRotation; + } + + @Override + public boolean readOnly() { + return this.builder.readOnly; + } +} From 7ebe1d8fec43c22decf5214405311b383cabdfa5 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 2 Sep 2026 19:40:18 +0200 Subject: [PATCH 08/19] [Edge] Fix Hardy Barth cPH1 EVSE: replace channel read with field channel() type parameter must extend Channel, not the value type. Track isEvConnected as a plain boolean field updated in onChargeControlStatus(). --- .../hardybarth/ecb1/EvseChargePointHardyBarthEcb1Impl.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index b40a768554a..15f9837f5e9 100644 --- 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 @@ -43,6 +43,7 @@ public class EvseChargePointHardyBarthEcb1Impl extends AbstractOpenemsComponent private Config config; private Ecb1Handler handler; + private boolean isEvConnected = false; public EvseChargePointHardyBarthEcb1Impl() { super(// @@ -80,6 +81,7 @@ protected void deactivate() { 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); } @@ -94,7 +96,6 @@ public ChargePointAbilities getChargePointAbilities() { return ChargePointAbilities.create().build(); } - final var connected = this.channel(EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED).value().orElse(false); final var phaseCount = evaluatePhaseCountFromCurrent(// this.getCurrentL1().orElse(0), // this.getCurrentL2().orElse(0), // @@ -107,7 +108,7 @@ public ChargePointAbilities getChargePointAbilities() { .setApplySetPoint(new ApplySetPoint.Ability.Ampere(phase, // this.config.minHwCurrent() / 1000, // this.config.maxHwCurrent() / 1000)) // - .setIsEvConnected(connected) // + .setIsEvConnected(this.isEvConnected) // .setIsReadyForCharging(this.getIsReadyForCharging()) // .build(); } From 9b7a9cc2549ffa00a9319a581194d081f4eca115 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 2 Sep 2026 19:48:44 +0200 Subject: [PATCH 09/19] [Edge] Fix Hardy Barth cPH1: make handler test methods public handleChargeControlResponse/handleMeterResponse must be public so the EVSE test (different package from Ecb1Handler) can call them directly. --- .../src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java index 8e1f588b688..33e0957629a 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java @@ -152,7 +152,7 @@ private String chargeControlUrl() { * * @param body raw JSON response body */ - void handleChargeControlResponse(String body) { + public void handleChargeControlResponse(String body) { JsonObject json; try { json = JsonUtils.parseToJsonObject(body); @@ -199,7 +199,7 @@ void handleChargeControlResponse(String body) { * * @param body raw JSON response body */ - void handleMeterResponse(String body) { + public void handleMeterResponse(String body) { JsonObject json; try { json = JsonUtils.parseToJsonObject(body); From 2d448ecfb159e27ef289e4f0a1eb167644770891 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 2 Sep 2026 19:58:13 +0200 Subject: [PATCH 10/19] [Edge] Fix Hardy Barth cPH1 EVSE: add missing Javadoc on MyConfig.create() --- .../edge/evse/chargepoint/hardybarth/ecb1/MyConfig.java | 5 +++++ 1 file changed, 5 insertions(+) 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 index f805666c89f..e9d61014357 100644 --- 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 @@ -64,6 +64,11 @@ public MyConfig build() { } } + /** + * Creates a Config builder. + * + * @return a {@link Builder} + */ public static Builder create() { return new Builder(); } From 4c7886ac5d0f89ffd4080f7513d7d31fadee8105 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 2 Sep 2026 20:30:52 +0200 Subject: [PATCH 11/19] [Edge] Hardy Barth cPH1 EVSE: add tests for getChargePointAbilities and apply --- ...EvseChargePointHardyBarthEcb1ImplTest.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) 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 index 1a11cd77a78..e9011071f60 100644 --- 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 @@ -1,7 +1,10 @@ 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; @@ -14,9 +17,13 @@ import io.openems.edge.bridge.http.cycle.dummy.DummyCycleSubscriber; 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.evcs.hardybarth.ecb1.Ecb1Handler; import io.openems.edge.evcs.hardybarth.ecb1.EvcsHardyBarthEcb1; 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 { @@ -206,6 +213,84 @@ void testSetCurrentSendsHttpRequests() throws Exception { "Expected manualmodeamp=10"); } + @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) // + .setMinHwCurrent(6_000) // + .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) // + .setMinHwCurrent(6_000) // + .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); From c534eb389f5ce101b93a307834a0a50410726f1f Mon Sep 17 00:00:00 2001 From: Stefan Feilmeier Date: Thu, 3 Sep 2026 09:10:14 +0200 Subject: [PATCH 12/19] Apply tools/prepare-commit.sh --- io.openems.edge.evse.chargepoint.alfen/.classpath | 1 + 1 file changed, 1 insertion(+) diff --git a/io.openems.edge.evse.chargepoint.alfen/.classpath b/io.openems.edge.evse.chargepoint.alfen/.classpath index 85263b03058..b4cffd0fe60 100644 --- a/io.openems.edge.evse.chargepoint.alfen/.classpath +++ b/io.openems.edge.evse.chargepoint.alfen/.classpath @@ -1,5 +1,6 @@ + From fbb0a8aae063188e8e1f497af7012c73f3577a23 Mon Sep 17 00:00:00 2001 From: Christoph Date: Thu, 3 Sep 2026 11:28:06 +0200 Subject: [PATCH 13/19] [Edge] Hardy Barth cPH1: remove EVCS, move Ecb1Handler/Parent to EVSE package Drops the legacy EVCS component (Evcs.HardyBarth.cPH1) and its tests. Moves Ecb1Handler and Ecb1Parent into the EVSE package; integrates the ECB1 channel definitions into EvseChargePointHardyBarthEcb1. The EVSE component (Evse.ChargePoint.HardyBarth.cPH1) is now self-contained. --- .../readme.adoc | 22 +- .../edge/evcs/hardybarth/ecb1/Config.java | 36 -- .../hardybarth/ecb1/EvcsHardyBarthEcb1.java | 89 ----- .../ecb1/EvcsHardyBarthEcb1Impl.java | 179 --------- .../hardybarth/ecb1/Ecb1Handler.java | 24 +- .../hardybarth/ecb1/Ecb1Parent.java | 6 +- .../ecb1/EvseChargePointHardyBarthEcb1.java | 80 ++++ .../EvseChargePointHardyBarthEcb1Impl.java | 5 +- .../ecb1/EvcsHardyBarthEcb1ImplTest.java | 351 ------------------ .../edge/evcs/hardybarth/ecb1/MyConfig.java | 94 ----- ...EvseChargePointHardyBarthEcb1ImplTest.java | 12 +- 11 files changed, 110 insertions(+), 788 deletions(-) delete mode 100644 io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Config.java delete mode 100644 io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1.java delete mode 100644 io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java rename io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/{evcs => evse/chargepoint}/hardybarth/ecb1/Ecb1Handler.java (89%) rename io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/{evcs => evse/chargepoint}/hardybarth/ecb1/Ecb1Parent.java (78%) delete mode 100644 io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1ImplTest.java delete mode 100644 io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/MyConfig.java diff --git a/io.openems.edge.evse.chargepoint.hardybarth/readme.adoc b/io.openems.edge.evse.chargepoint.hardybarth/readme.adoc index 52cb421d3ac..76a5a0af66e 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/readme.adoc +++ b/io.openems.edge.evse.chargepoint.hardybarth/readme.adoc @@ -7,7 +7,7 @@ Three-phase AC charging stations with REST API integration. This bundle provides integration with Hardy Barth charging stations. Two device families are supported, each using a different REST API: * **Hardy Barth Salia** (`Evcs.HardyBarth`, `Evse.ChargePoint.HardyBarth`) – Salia controller API (`/api/secc`) -* **Hardy Barth cPH1 (ECB1)** (`Evcs.HardyBarth.cPH1`) – eCharge ECB1 REST API (`/api/v1/chargecontrols/`, `/api/v1/meters/`) +* **Hardy Barth cPH1 (ECB1)** (`Evse.ChargePoint.HardyBarth.cPH1`) – eCharge ECB1 REST API (`/api/v1/chargecontrols/`, `/api/v1/meters/`) == Supported Devices @@ -84,27 +84,25 @@ This bundle implements the following OpenEMS Components: [[_evcs_hardy]] -=== <<_evcs_cph1,EVCS Hardy Barth cPH1>> +=== <<_evse_cph1,EVSE Charge-Point Hardy Barth cPH1>> -*Name*: EVCS Hardy Barth cPH1 +*Name*: EVSE Charge-Point Hardy Barth cPH1 -*Factory-PID*: `Evcs.HardyBarth.cPH1` +*Factory-PID*: `Evse.ChargePoint.HardyBarth.cPH1` .Implemented Natures/Interfaces -* EvcsHardyBarthEcb1 +* EvseChargePointHardyBarthEcb1 * OpenemsComponent -* EventHandler -* Evcs -* ManagedEvcs +* EvseChargePoint * ElectricityMeter -*Description*: EVCS interface for the Hardy Barth eCharge cPH1 wallbox (EVCC Basic controller, ECB1 REST API). +*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 +* `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") @@ -112,6 +110,8 @@ Write operations (start, stop, set current) are dispatched as HTTP POST requests * `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 |=== @@ -142,6 +142,6 @@ Write operations (start, stop, set current) are dispatched as HTTP POST requests | `/api/v1/chargecontrols/{id}/mode/manual/ampere` (`manualmodeamp={A}`) |=== -[[_evcs_cph1]] +[[_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/evcs/hardybarth/ecb1/Config.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Config.java deleted file mode 100644 index cc66836bd3b..00000000000 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Config.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.openems.edge.evcs.hardybarth.ecb1; - -import org.osgi.service.metatype.annotations.AttributeDefinition; -import org.osgi.service.metatype.annotations.ObjectClassDefinition; - -@ObjectClassDefinition(// - name = "EVCS 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 "evcs0"; - - @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 = "Minimum hardware current", description = "Minimum current of the Charger in mA.", required = true) - int minHwCurrent() default 6000; - - @AttributeDefinition(name = "Maximum hardware current", description = "Maximum current of the Charger in mA.", required = true) - int maxHwCurrent() default 32000; - - String webconsole_configurationFactory_nameHint() default "EVCS Hardy Barth cPH1 [{id}]"; -} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1.java deleted file mode 100644 index a2c98b2a611..00000000000 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1.java +++ /dev/null @@ -1,89 +0,0 @@ -package io.openems.edge.evcs.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.evcs.api.Evcs; -import io.openems.edge.evcs.api.ManagedEvcs; -import io.openems.edge.meter.api.ElectricityMeter; - -public interface EvcsHardyBarthEcb1 extends OpenemsComponent, Evcs, ManagedEvcs, 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/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java deleted file mode 100644 index daad403a7b5..00000000000 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1Impl.java +++ /dev/null @@ -1,179 +0,0 @@ -package io.openems.edge.evcs.hardybarth.ecb1; - -import static io.openems.edge.evcs.api.ChargingType.AC; -import static io.openems.edge.evcs.api.Phases.THREE_PHASE; -import static java.lang.Math.round; - -import org.osgi.service.component.ComponentContext; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.ConfigurationPolicy; -import org.osgi.service.component.annotations.Deactivate; -import org.osgi.service.component.annotations.Reference; -import org.osgi.service.event.Event; -import org.osgi.service.event.EventHandler; -import org.osgi.service.event.propertytypes.EventTopics; -import org.osgi.service.metatype.annotations.Designate; - -import io.openems.common.bridge.http.api.BridgeHttpFactory; -import io.openems.common.exceptions.OpenemsException; -import io.openems.common.types.MeterType; -import io.openems.edge.bridge.http.cycle.HttpBridgeCycleServiceDefinition; -import io.openems.edge.common.component.OpenemsComponent; -import io.openems.edge.common.event.EdgeEventConstants; -import io.openems.edge.evcs.api.AbstractManagedEvcsComponent; -import io.openems.edge.evcs.api.Evcs; -import io.openems.edge.evcs.api.EvcsPower; -import io.openems.edge.evcs.api.ManagedEvcs; -import io.openems.edge.evcs.api.Status; -import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.PhaseRotation; - -@Designate(ocd = Config.class, factory = true) -@Component(// - name = "Evcs.HardyBarth.cPH1", // - immediate = true, // - configurationPolicy = ConfigurationPolicy.REQUIRE // -) -@EventTopics({ // - EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE, // - EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE // -}) -public class EvcsHardyBarthEcb1Impl extends AbstractManagedEvcsComponent - implements EvcsHardyBarthEcb1, Ecb1Parent, OpenemsComponent, EventHandler, ManagedEvcs, Evcs, - ElectricityMeter { - - @Reference - private BridgeHttpFactory httpBridgeFactory; - - @Reference - private HttpBridgeCycleServiceDefinition httpBridgeCycleServiceDefinition; - - @Reference - private EvcsPower evcsPower; - - private Config config; - private Ecb1Handler handler; - - public EvcsHardyBarthEcb1Impl() { - super(// - OpenemsComponent.ChannelId.values(), // - ElectricityMeter.ChannelId.values(), // - Evcs.ChannelId.values(), // - ManagedEvcs.ChannelId.values(), // - EvcsHardyBarthEcb1.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()); - - this._setChargingType(AC); - this._setFixedMinimumHardwarePower(round(config.minHwCurrent() / 1000f) * DEFAULT_VOLTAGE * THREE_PHASE.getValue()); - this._setFixedMaximumHardwarePower(round(config.maxHwCurrent() / 1000f) * DEFAULT_VOLTAGE * THREE_PHASE.getValue()); - this._setPowerPrecision(230); - this._setPhases(THREE_PHASE); - - this.handler = new Ecb1Handler(this, config.ip(), config.chargeControlId(), config.meterId(), - this.httpBridgeFactory, this.httpBridgeCycleServiceDefinition); - } - - @Override - @Deactivate - protected void deactivate() { - super.deactivate(); - if (this.handler != null) { - this.handler.deactivate(); - } - } - - @Override - public void handleEvent(Event event) { - if (!this.isEnabled()) { - return; - } - super.handleEvent(event); - } - - @Override - public void onChargeControlStatus(String state, Integer stateId, Boolean connected) { - this._setStatus(toStatus(state, stateId)); - } - - @Override - public void onCommunicationFailed(boolean failed) { - this._setChargingstationCommunicationFailed(failed); - } - - private static Status toStatus(String state, Integer stateId) { - if (state == null || state.isEmpty()) { - return Status.UNDEFINED; - } - return switch (state.charAt(0)) { - case 'A' -> Status.NOT_READY_FOR_CHARGING; - case 'B' -> stateId != null && stateId == 17 ? Status.CHARGING_REJECTED : Status.READY_FOR_CHARGING; - case 'C', 'D' -> Status.CHARGING; - case 'E', 'F' -> Status.ERROR; - default -> Status.UNDEFINED; - }; - } - - @Override - public boolean applyChargePowerLimit(int power) throws OpenemsException { - var phases = this.getPhasesAsInt(); - var currentA = (int) round(power / (float) phases / 230.0f); - var minA = this.config.minHwCurrent() / 1000; - var maxA = this.config.maxHwCurrent() / 1000; - currentA = Math.max(minA, Math.min(currentA, maxA)); - return this.handler.setTarget(currentA); - } - - @Override - public boolean pauseChargeProcess() throws OpenemsException { - return this.handler.setTarget(0); - } - - @Override - public boolean applyDisplayText(String text) throws OpenemsException { - return false; - } - - @Override - public int getMinimumTimeTillChargingLimitTaken() { - return 30; - } - - @Override - public int getConfiguredMinimumHardwarePower() { - return round(this.config.minHwCurrent() / 1000f) * DEFAULT_VOLTAGE * THREE_PHASE.getValue(); - } - - @Override - public int getConfiguredMaximumHardwarePower() { - return round(this.config.maxHwCurrent() / 1000f) * DEFAULT_VOLTAGE * THREE_PHASE.getValue(); - } - - @Override - public EvcsPower getEvcsPower() { - return this.evcsPower; - } - - @Override - public boolean getConfiguredDebugMode() { - return false; - } - - @Override - public MeterType getMeterType() { - return MeterType.MANAGED_CONSUMPTION_METERED; - } - - @Override - public PhaseRotation getPhaseRotation() { - return PhaseRotation.L1_L2_L3; - } -} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Ecb1Handler.java similarity index 89% rename from io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java rename to io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Ecb1Handler.java index 33e0957629a..3a730a9a6fc 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Handler.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Ecb1Handler.java @@ -1,4 +1,4 @@ -package io.openems.edge.evcs.hardybarth.ecb1; +package io.openems.edge.evse.chargepoint.hardybarth.ecb1; import static io.openems.common.bridge.http.api.HttpMethod.POST; import static io.openems.edge.common.channel.ChannelUtils.setValue; @@ -167,17 +167,17 @@ public void handleChargeControlResponse(String body) { } var stateId = getIntOrNull(cc, "stateid"); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_STATE_ID, stateId); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE_ID, stateId); var state = getStringOrNull(cc, "state"); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_STATE, state); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE, state); var mode = getStringOrNull(cc, "mode"); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_MODE, mode); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_MODE, mode); var connected = getBooleanOrNull(cc, "connected"); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED, connected); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_MANUAL_MODE_AMP, getDoubleOrNull(cc, "manualmodeamp")); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_CURRENT_PWM_AMP, getDoubleOrNull(cc, "currentpwmamp")); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_VENDOR, getStringOrNull(cc, "vendor")); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_VERSION, getStringOrNull(cc, "version")); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_CONNECTED, connected); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_MANUAL_MODE_AMP, getDoubleOrNull(cc, "manualmodeamp")); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_CURRENT_PWM_AMP, getDoubleOrNull(cc, "currentpwmamp")); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_VENDOR, getStringOrNull(cc, "vendor")); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_VERSION, getStringOrNull(cc, "version")); // Re-set manual mode if the device has drifted to another mode if (mode != null && !mode.equals("manual")) { @@ -218,9 +218,9 @@ public void handleMeterResponse(String body) { return; } - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_METER_SERIAL, getIntOrNull(meter, "serial")); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, getStringOrNull(meter, "vendor")); - setValue(this.parent, EvcsHardyBarthEcb1.ChannelId.RAW_METER_TYPE, getStringOrNull(meter, "type")); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_SERIAL, getIntOrNull(meter, "serial")); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, getStringOrNull(meter, "vendor")); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_TYPE, getStringOrNull(meter, "type")); // Active power (W) var powerTotal = roundToInt(getObisDouble(data, OBIS_POWER_TOTAL)); diff --git a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Parent.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Ecb1Parent.java similarity index 78% rename from io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Parent.java rename to io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Ecb1Parent.java index be1b3745f3c..c1c6e7d5159 100644 --- a/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evcs/hardybarth/ecb1/Ecb1Parent.java +++ b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Ecb1Parent.java @@ -1,12 +1,8 @@ -package io.openems.edge.evcs.hardybarth.ecb1; +package io.openems.edge.evse.chargepoint.hardybarth.ecb1; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.meter.api.ElectricityMeter; -/** - * Common parent interface for components driven by the Hardy Barth ECB1 REST - * API. Implemented by both the legacy EVCS and the new EVSE component. - */ public interface Ecb1Parent extends OpenemsComponent, ElectricityMeter { /** 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 index d2f1310fee4..64890efecc2 100644 --- 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 @@ -1,8 +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 index 15f9837f5e9..94d3d845b7e 100644 --- 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 @@ -16,9 +16,6 @@ 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.evcs.hardybarth.ecb1.Ecb1Handler; -import io.openems.edge.evcs.hardybarth.ecb1.Ecb1Parent; -import io.openems.edge.evcs.hardybarth.ecb1.EvcsHardyBarthEcb1; 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; @@ -50,7 +47,7 @@ public EvseChargePointHardyBarthEcb1Impl() { OpenemsComponent.ChannelId.values(), // ElectricityMeter.ChannelId.values(), // EvseChargePoint.ChannelId.values(), // - EvcsHardyBarthEcb1.ChannelId.values() // + EvseChargePointHardyBarthEcb1.ChannelId.values() // ); ElectricityMeter.calculateSumCurrentFromPhases(this); ElectricityMeter.calculateAverageVoltageFromPhases(this); diff --git a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1ImplTest.java b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1ImplTest.java deleted file mode 100644 index d51515b8cbd..00000000000 --- a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/EvcsHardyBarthEcb1ImplTest.java +++ /dev/null @@ -1,351 +0,0 @@ -package io.openems.edge.evcs.hardybarth.ecb1; - -import static io.openems.common.bridge.http.dummy.DummyBridgeHttpFactory.ofBridgeImpl; -import static io.openems.edge.evcs.api.Phases.THREE_PHASE; -import static io.openems.edge.evcs.api.Status.CHARGING; -import static io.openems.edge.evcs.api.Status.NOT_READY_FOR_CHARGING; -import static org.junit.jupiter.api.Assertions.assertFalse; -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.utils.ReflectionUtils; -import io.openems.edge.bridge.http.cycle.HttpBridgeCycleServiceDefinition; -import io.openems.edge.bridge.http.cycle.dummy.DummyCycleSubscriber; -import io.openems.edge.common.test.AbstractComponentTest.TestCase; -import io.openems.edge.common.test.ComponentTest; -import io.openems.edge.evcs.api.ChargingType; -import io.openems.edge.evcs.api.Evcs; -import io.openems.edge.evcs.api.Status; -import io.openems.edge.meter.api.ElectricityMeter; - -class EvcsHardyBarthEcb1ImplTest { - - /** Chargecontrol response for a stopped (no car) scenario – from a real device. */ - private static final String CHARGECONTROL_NO_CAR = """ - { - "chargecontrol": { - "modeid": 3, - "evminamp": 6, - "vendor": "Phoenix Contact", - "name": "evcc1", - "mode": "manual", - "type": "EVCC Basic (RS485)", - "id": 1, - "supplylinemaxamp": 32, - "connected": false, - "manualmodeamp": 10.0, - "stateid": 17, - "version": "V1.3.1", - "busid": 1, - "currentpwmamp": 0.0, - "state": "A\\u2019" - }, - "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" - } - """; - - /** Meter response from a real device (standby values). */ - private static final String METER_RESPONSE = """ - { - "meter": { - "serial": 75740051, - "vendor": "eCHARGE", - "type": "eCB1 intern", - "name": "Carport", - "data": { - "1-0:1.4.0": 3.5, - "1-0:1.8.0": 6541.3684, - "1-0:21.4.0": 3.5, - "1-0:41.4.0": 0.0, - "1-0:61.4.0": 0.0, - "1-0:31.4.0": 0.018, - "1-0:51.4.0": 0.0, - "1-0:71.4.0": 0.0, - "1-0:32.4.0": 228.373, - "1-0:52.4.0": 229.971, - "1-0:72.4.0": 228.620 - }, - "id": 1, - "ipaddress": "127.0.0.1", - "function": "socket" - }, - "protocol-version": "1.4" - } - """; - - /** Meter response for a charging scenario with clean numbers. */ - 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, - "ipaddress": "127.0.0.1", - "function": "socket" - }, - "protocol-version": "1.4" - } - """; - - @Test - void testNoCar() throws Exception { - var sut = new EvcsHardyBarthEcb1Impl(); - var test = new ComponentTest(sut) // - .addReference("httpBridgeFactory", - ofBridgeImpl(DummyBridgeHttpFactory::dummyEndpointFetcher, - DummyBridgeHttpFactory::dummyBridgeHttpExecutor)) // - .addReference("httpBridgeCycleServiceDefinition", - new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // - .activate(MyConfig.create() // - .setId("evcs0") // - .setIp("192.168.2.8") // - .setChargeControlId(1) // - .setMeterId(1) // - .setMinHwCurrent(6_000) // - .setMaxHwCurrent(32_000) // - .build()); - var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); - test // - .next(new TestCase() // - .onBeforeProcessImage(() -> { - handler.handleChargeControlResponse(CHARGECONTROL_NO_CAR); - handler.handleMeterResponse(METER_RESPONSE); - }) // - .output(Evcs.ChannelId.STATUS, NOT_READY_FOR_CHARGING) // - .output(Evcs.ChannelId.CHARGINGSTATION_COMMUNICATION_FAILED, false) // - .output(Evcs.ChannelId.CHARGING_TYPE, ChargingType.AC) // - .output(Evcs.ChannelId.PHASES, THREE_PHASE) // - .output(Evcs.ChannelId.FIXED_MINIMUM_HARDWARE_POWER, 4140) // - .output(Evcs.ChannelId.FIXED_MAXIMUM_HARDWARE_POWER, 22080) // - - .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 4) // - .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, 4) // - .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, 0) // - .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, 0) // - .output(ElectricityMeter.ChannelId.CURRENT, 18) // - .output(ElectricityMeter.ChannelId.CURRENT_L1, 18) // - .output(ElectricityMeter.ChannelId.CURRENT_L2, 0) // - .output(ElectricityMeter.ChannelId.CURRENT_L3, 0) // - .output(ElectricityMeter.ChannelId.VOLTAGE, 228_988) // - .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 228_373) // - .output(ElectricityMeter.ChannelId.VOLTAGE_L2, 229_971) // - .output(ElectricityMeter.ChannelId.VOLTAGE_L3, 228_620) // - .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY, 6541L) // - .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, 6541L) // - - .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE_ID, 17) // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE, "A’") // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_MODE, "manual") // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED, false) // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_MANUAL_MODE_AMP, 10.0) // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_CURRENT_PWM_AMP, 0.0) // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_VENDOR, "Phoenix Contact") // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_VERSION, "V1.3.1") // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_METER_SERIAL, 75740051) // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, "eCHARGE") // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_METER_TYPE, "eCB1 intern") // - ); - } - - @Test - void testCharging() throws Exception { - var sut = new EvcsHardyBarthEcb1Impl(); - var test = new ComponentTest(sut) // - .addReference("httpBridgeFactory", - ofBridgeImpl(DummyBridgeHttpFactory::dummyEndpointFetcher, - DummyBridgeHttpFactory::dummyBridgeHttpExecutor)) // - .addReference("httpBridgeCycleServiceDefinition", - new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // - .activate(MyConfig.create() // - .setId("evcs0") // - .setIp("192.168.2.8") // - .setChargeControlId(1) // - .setMeterId(1) // - .setMinHwCurrent(6_000) // - .setMaxHwCurrent(32_000) // - .build()); - var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); - test // - .next(new TestCase() // - .onBeforeProcessImage(() -> { - handler.handleChargeControlResponse(CHARGECONTROL_CHARGING); - handler.handleMeterResponse(METER_CHARGING); - }) // - .output(Evcs.ChannelId.STATUS, CHARGING) // - .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_L1, 16_000) // - .output(ElectricityMeter.ChannelId.CURRENT_L2, 16_000) // - .output(ElectricityMeter.ChannelId.CURRENT_L3, 16_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, 10000L) // - - .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE_ID, 5) // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE, "C") // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED, true) // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_MANUAL_MODE_AMP, 16.0) // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_CURRENT_PWM_AMP, 16.0) // - ); - } - - @Test - void testCarConnectedPaused() throws Exception { - var sut = new EvcsHardyBarthEcb1Impl(); - var test = new ComponentTest(sut) // - .addReference("httpBridgeFactory", - ofBridgeImpl(DummyBridgeHttpFactory::dummyEndpointFetcher, - DummyBridgeHttpFactory::dummyBridgeHttpExecutor)) // - .addReference("httpBridgeCycleServiceDefinition", - new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // - .activate(MyConfig.create() // - .setId("evcs0") // - .setIp("192.168.2.8") // - .setChargeControlId(1) // - .setMeterId(1) // - .setMinHwCurrent(6_000) // - .setMaxHwCurrent(32_000) // - .build()); - var handler = ReflectionUtils.getValueViaReflection(sut, "handler"); - test // - .next(new TestCase() // - .onBeforeProcessImage(() -> handler.handleChargeControlResponse(CHARGECONTROL_CAR_PAUSED)) // - .output(Evcs.ChannelId.STATUS, Status.CHARGING_REJECTED) // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE, "B") // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED, true) // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE_ID, 17) // - ); - } - - @Test - void testSetCurrentSendsHttpRequests() throws Exception { - final var pool = DummyBridgeHttpFactory.dummyBridgeHttpExecutor(false); - final var httpBundle = DummyBridgeHttpBundle.of(pool); - final var sentBodies = new java.util.ArrayList(); - final var sentUrls = 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 EvcsHardyBarthEcb1Impl(); - new ComponentTest(sut) // - .addReference("httpBridgeFactory", httpBundle.factory()) // - .addReference("httpBridgeCycleServiceDefinition", - new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // - .activate(MyConfig.create() // - .setId("evcs0") // - .setIp("192.168.2.8") // - .setChargeControlId(1) // - .setMeterId(1) // - .setMinHwCurrent(6_000) // - .setMaxHwCurrent(32_000) // - .build()); - - // Trigger applyChargePowerLimit at 6900 W (3 phases × 10 A × 230 V) - sut.applyChargePowerLimit(6900); - - // Flush the executor so requests are actually dispatched - pool.update(); - - // Expect set-current and start requests - 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 (6900 W / 3 / 230 = 10 A)"); - } - - @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 EvcsHardyBarthEcb1Impl(); - new ComponentTest(sut) // - .addReference("httpBridgeFactory", httpBundle.factory()) // - .addReference("httpBridgeCycleServiceDefinition", - new HttpBridgeCycleServiceDefinition(new DummyCycleSubscriber())) // - .activate(MyConfig.create() // - .setId("evcs0") // - .setIp("192.168.2.8") // - .setChargeControlId(1) // - .setMeterId(1) // - .setMinHwCurrent(6_000) // - .setMaxHwCurrent(32_000) // - .build()); - - sut.pauseChargeProcess(); - pool.update(); - - assertTrue(sentUrls.stream().anyMatch(u -> u.contains("/stop")), - "Expected a stop POST"); - } -} diff --git a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/MyConfig.java b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/MyConfig.java deleted file mode 100644 index 7f4e609327e..00000000000 --- a/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evcs/hardybarth/ecb1/MyConfig.java +++ /dev/null @@ -1,94 +0,0 @@ -package io.openems.edge.evcs.hardybarth.ecb1; - -import io.openems.common.test.AbstractComponentConfig; - -@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 minHwCurrent; - private int maxHwCurrent; - - 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 setMinHwCurrent(int minHwCurrent) { - this.minHwCurrent = minHwCurrent; - return this; - } - - public Builder setMaxHwCurrent(int maxHwCurrent) { - this.maxHwCurrent = maxHwCurrent; - 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 minHwCurrent() { - return this.builder.minHwCurrent; - } - - @Override - public int maxHwCurrent() { - return this.builder.maxHwCurrent; - } -} 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 index e9011071f60..113143de3b6 100644 --- 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 @@ -19,8 +19,6 @@ 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.evcs.hardybarth.ecb1.Ecb1Handler; -import io.openems.edge.evcs.hardybarth.ecb1.EvcsHardyBarthEcb1; 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; @@ -132,8 +130,8 @@ void testNoCar() throws Exception { .next(new TestCase() // .onBeforeProcessImage(() -> handler.handleChargeControlResponse(CHARGECONTROL_NO_CAR)) // .output(EvseChargePoint.ChannelId.IS_READY_FOR_CHARGING, false) // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE, "A") // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED, false) // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE, "A") // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_CONNECTED, false) // ); } @@ -146,8 +144,8 @@ void testCarPaused() throws Exception { .next(new TestCase() // .onBeforeProcessImage(() -> handler.handleChargeControlResponse(CHARGECONTROL_CAR_PAUSED)) // .output(EvseChargePoint.ChannelId.IS_READY_FOR_CHARGING, true) // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE, "B") // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_CONNECTED, true) // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE, "B") // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_CONNECTED, true) // ); } @@ -163,7 +161,7 @@ void testCharging() throws Exception { handler.handleMeterResponse(METER_CHARGING); }) // .output(EvseChargePoint.ChannelId.IS_READY_FOR_CHARGING, true) // - .output(EvcsHardyBarthEcb1.ChannelId.RAW_STATE, "C") // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE, "C") // .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 11040) // .output(ElectricityMeter.ChannelId.CURRENT_L1, 16_000) // .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 230_000) // From 88ec6cde699fc300fcbe1c426fea12318facce42 Mon Sep 17 00:00:00 2001 From: Christoph-87 <77729959+Christoph-87@users.noreply.github.com> Date: Sun, 20 Sep 2026 20:48:20 +0000 Subject: [PATCH 14/19] fix(evse): address Hardy Barth cPH1 review --- .../chargepoint/hardybarth/ecb1/Config.java | 3 - .../hardybarth/ecb1/Ecb1Handler.java | 72 ++++++++----------- .../EvseChargePointHardyBarthEcb1Impl.java | 4 +- ...EvseChargePointHardyBarthEcb1ImplTest.java | 39 ++++++++-- .../chargepoint/hardybarth/ecb1/MyConfig.java | 11 --- 5 files changed, 66 insertions(+), 63 deletions(-) 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 index 028e911dbd7..39e7fc740c3 100644 --- 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 @@ -28,9 +28,6 @@ @AttributeDefinition(name = "Meter-ID", description = "The ID of the energy meter (default: 1).", required = true) int meterId() default 1; - @AttributeDefinition(name = "Minimum hardware current", description = "Minimum current of the Charger in mA.", required = true) - int minHwCurrent() default 6000; - @AttributeDefinition(name = "Maximum hardware current", description = "Maximum current of the Charger in mA.", required = true) int maxHwCurrent() default 32000; 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 index 3a730a9a6fc..a8f867f9171 100644 --- 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 @@ -205,16 +205,19 @@ public void handleMeterResponse(String body) { json = JsonUtils.parseToJsonObject(body); } catch (Exception e) { this.log.warn("Cannot parse ECB1 meter response: " + e.getMessage()); + this.clearMeterValues(); return; } - var meter = json.getAsJsonObject("meter"); + var meter = JsonUtils.getAsOptionalJsonObject(json, "meter").orElse(null); if (meter == null) { + this.clearMeterValues(); return; } - var data = meter.getAsJsonObject("data"); + var data = JsonUtils.getAsOptionalJsonObject(meter, "data").orElse(null); if (data == null) { + this.clearMeterValues(); return; } @@ -258,20 +261,31 @@ public void handleMeterResponse(String body) { this.parent._setActiveConsumptionEnergy(energyWhLong); } + private void clearMeterValues() { + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_SERIAL, null); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, null); + setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_TYPE, null); + + this.parent._setActivePower(null); + this.parent._setActivePowerL1(null); + this.parent._setActivePowerL2(null); + this.parent._setActivePowerL3(null); + this.parent._setCurrentL1(null); + this.parent._setCurrentL2(null); + this.parent._setCurrentL3(null); + this.parent._setVoltageL1(null); + this.parent._setVoltageL2(null); + this.parent._setVoltageL3(null); + this.parent._setActiveProductionEnergy(null); + this.parent._setActiveConsumptionEnergy(null); + } + // ------------------------------------------------------------------------- // JSON helpers // ------------------------------------------------------------------------- private static Double getObisDouble(JsonObject data, String obisCode) { - var element = data.get(obisCode); - if (element == null || element.isJsonNull()) { - return null; - } - try { - return element.getAsDouble(); - } catch (Exception e) { - return null; - } + return JsonUtils.getAsOptionalDouble(data, obisCode).orElse(null); } private static Integer roundToInt(Double value) { @@ -283,46 +297,18 @@ private static Double scale(Double value, double factor) { } private static Integer getIntOrNull(JsonObject obj, String key) { - var el = obj.get(key); - if (el == null || el.isJsonNull()) { - return null; - } - try { - return el.getAsInt(); - } catch (Exception e) { - return null; - } + return JsonUtils.getAsOptionalInt(obj, key).orElse(null); } private static String getStringOrNull(JsonObject obj, String key) { - var el = obj.get(key); - if (el == null || el.isJsonNull()) { - return null; - } - return el.getAsString(); + return JsonUtils.getAsOptionalString(obj, key).orElse(null); } private static Boolean getBooleanOrNull(JsonObject obj, String key) { - var el = obj.get(key); - if (el == null || el.isJsonNull()) { - return null; - } - try { - return el.getAsBoolean(); - } catch (Exception e) { - return null; - } + return JsonUtils.getAsOptionalBoolean(obj, key).orElse(null); } private static Double getDoubleOrNull(JsonObject obj, String key) { - var el = obj.get(key); - if (el == null || el.isJsonNull()) { - return null; - } - try { - return el.getAsDouble(); - } catch (Exception e) { - return null; - } + return JsonUtils.getAsOptionalDouble(obj, key).orElse(null); } } 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 index 94d3d845b7e..576eb1f28be 100644 --- 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 @@ -32,6 +32,8 @@ public class EvseChargePointHardyBarthEcb1Impl extends AbstractOpenemsComponent implements EvseChargePointHardyBarthEcb1, Ecb1Parent, OpenemsComponent, EvseChargePoint, ElectricityMeter { + private static final int MIN_CURRENT_A = 6; + @Reference private BridgeHttpFactory httpBridgeFactory; @@ -103,7 +105,7 @@ public ChargePointAbilities getChargePointAbilities() { return ChargePointAbilities.create() // .setApplySetPoint(new ApplySetPoint.Ability.Ampere(phase, // - this.config.minHwCurrent() / 1000, // + MIN_CURRENT_A, // this.config.maxHwCurrent() / 1000)) // .setIsEvConnected(this.isEvConnected) // .setIsReadyForCharging(this.getIsReadyForCharging()) // 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 index 113143de3b6..f0ae815f0a0 100644 --- 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 @@ -104,6 +104,18 @@ class EvseChargePointHardyBarthEcb1ImplTest { } """; + /** 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", @@ -116,7 +128,6 @@ private static ComponentTest buildTest(EvseChargePointHardyBarthEcb1Impl sut) th .setIp("192.168.2.8") // .setChargeControlId(1) // .setMeterId(1) // - .setMinHwCurrent(6_000) // .setMaxHwCurrent(32_000) // .build()); } @@ -169,6 +180,28 @@ void testCharging() throws Exception { ); } + @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(EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, "eCHARGE") // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 11040) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, 16_000) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 230_000) // + .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY, 10000L)) // + .next(new TestCase() // + .onBeforeProcessImage(() -> handler.handleMeterResponse(METER_WITHOUT_DATA)) // + .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, null) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER, null) // + .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, null) // + .output(ElectricityMeter.ChannelId.CURRENT_L1, null) // + .output(ElectricityMeter.ChannelId.VOLTAGE_L1, null)); + } + @Test void testSetCurrentSendsHttpRequests() throws Exception { final var pool = DummyBridgeHttpFactory.dummyBridgeHttpExecutor(false); @@ -194,7 +227,6 @@ void testSetCurrentSendsHttpRequests() throws Exception { .setIp("192.168.2.8") // .setChargeControlId(1) // .setMeterId(1) // - .setMinHwCurrent(6_000) // .setMaxHwCurrent(32_000) // .build()); @@ -249,7 +281,6 @@ void testGetChargePointAbilitiesReadOnly() throws Exception { .setIp("192.168.2.8") // .setChargeControlId(1) // .setMeterId(1) // - .setMinHwCurrent(6_000) // .setMaxHwCurrent(32_000) // .setReadOnly(true) // .build()); @@ -278,7 +309,6 @@ void testApplyCallsHandler() throws Exception { .setIp("192.168.2.8") // .setChargeControlId(1) // .setMeterId(1) // - .setMinHwCurrent(6_000) // .setMaxHwCurrent(32_000) // .build()); @@ -310,7 +340,6 @@ void testPauseStopsCharging() throws Exception { .setIp("192.168.2.8") // .setChargeControlId(1) // .setMeterId(1) // - .setMinHwCurrent(6_000) // .setMaxHwCurrent(32_000) // .build()); 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 index e9d61014357..56bed694bb2 100644 --- 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 @@ -11,7 +11,6 @@ protected static class Builder { private String ip; private int chargeControlId; private int meterId; - private int minHwCurrent; private int maxHwCurrent; private PhaseRotation phaseRotation = PhaseRotation.L1_L2_L3; private boolean readOnly = false; @@ -39,11 +38,6 @@ public Builder setMeterId(int meterId) { return this; } - public Builder setMinHwCurrent(int minHwCurrent) { - this.minHwCurrent = minHwCurrent; - return this; - } - public Builder setMaxHwCurrent(int maxHwCurrent) { this.maxHwCurrent = maxHwCurrent; return this; @@ -95,11 +89,6 @@ public int meterId() { return this.builder.meterId; } - @Override - public int minHwCurrent() { - return this.builder.minHwCurrent; - } - @Override public int maxHwCurrent() { return this.builder.maxHwCurrent; From ca38332a6d0bbe92e5c993d025ac8bfc38bc9460 Mon Sep 17 00:00:00 2001 From: Christoph-87 <77729959+Christoph-87@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:28:13 +0000 Subject: [PATCH 15/19] test(evse): extend Hardy Barth cPH1 coverage --- ...EvseChargePointHardyBarthEcb1ImplTest.java | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) 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 index f0ae815f0a0..ac5d0f4c77b 100644 --- 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 @@ -77,6 +77,23 @@ class EvseChargePointHardyBarthEcb1ImplTest { } """; + /** 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 = """ { @@ -199,9 +216,44 @@ void testInvalidMeterResponseClearsPreviousValues() throws Exception { .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); @@ -243,6 +295,74 @@ void testSetCurrentSendsHttpRequests() throws Exception { "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(); From e11baf8e3ee56a8cc0ecbf9cb07ed22edadb9898 Mon Sep 17 00:00:00 2001 From: Stefan Feilmeier Date: Wed, 23 Sep 2026 21:58:12 +0200 Subject: [PATCH 16/19] Explicitely name cPH2 package --- .../hardybarth/{ => cph2}/ChargePointStatus.java | 2 +- .../evse/chargepoint/hardybarth/{ => cph2}/Config.java | 8 ++++---- .../hardybarth/{ => cph2}/EvseChargePointHardyBarth.java | 2 +- .../{ => cph2}/EvseChargePointHardyBarthImpl.java | 2 +- .../chargepoint/hardybarth/{ => cph2}/EvseHandler.java | 2 +- .../{ => cph2}/EvseChargePointHardyImplTest.java | 6 +++--- .../evse/chargepoint/hardybarth/{ => cph2}/MyConfig.java | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) rename io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/{ => cph2}/ChargePointStatus.java (92%) rename io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/{ => cph2}/Config.java (84%) rename io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/{ => cph2}/EvseChargePointHardyBarth.java (95%) rename io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/{ => cph2}/EvseChargePointHardyBarthImpl.java (98%) rename io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/{ => cph2}/EvseHandler.java (98%) rename io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/{ => cph2}/EvseChargePointHardyImplTest.java (98%) rename io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/{ => cph2}/MyConfig.java (96%) 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/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; From 847108a7d72756b7adffad90f34c06fffa153b6f Mon Sep 17 00:00:00 2001 From: Stefan Feilmeier Date: Wed, 23 Sep 2026 22:47:40 +0200 Subject: [PATCH 17/19] Improve usage of existing Utils --- .../io/openems/common/utils/JsonUtils.java | 48 ++++++ .../org.eclipse.core.resources.prefs | 4 +- .../hardybarth/common/DeviceRole.java | 8 +- .../hardybarth/common/HardyBarth.java | 31 ++-- .../hardybarth/ecb1/Ecb1Handler.java | 161 +++++------------- .../ecb1/EvseChargePointHardyBarthEcb1.java | 26 +-- .../hardybarth/EvcsHardyBarthImplTest.java | 10 +- .../hardybarth/common/TestData.java | 114 ++++++------- ...EvseChargePointHardyBarthEcb1ImplTest.java | 60 +++++-- 9 files changed, 240 insertions(+), 222 deletions(-) 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..70e4523fc6b 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.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/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/ecb1/Ecb1Handler.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/Ecb1Handler.java index a8f867f9171..aad3cfb1db3 100644 --- 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 @@ -1,8 +1,12 @@ 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 java.lang.Math.round; +import static io.openems.edge.common.type.TypeUtils.multiply; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -16,10 +20,15 @@ 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.jsonrpc.serialization.JsonObjectPath; +import io.openems.common.jsonrpc.serialization.JsonObjectPathActual; +import io.openems.common.jsonrpc.serialization.JsonObjectPathActual.JsonObjectPathActualNonNull; 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. @@ -158,34 +167,34 @@ public void handleChargeControlResponse(String body) { json = JsonUtils.parseToJsonObject(body); } catch (Exception e) { this.log.warn("Cannot parse ECB1 charge-control response: " + e.getMessage()); - return; + json = new JsonObject(); } - var cc = json.getAsJsonObject("chargecontrol"); - if (cc == null) { - return; - } + final var cc = JsonUtils.getAsOptionalJsonObject(json, "chargecontrol").orElse(null); + final var hb = this.parent; - var stateId = getIntOrNull(cc, "stateid"); - setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE_ID, stateId); - var state = getStringOrNull(cc, "state"); - setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE, state); - var mode = getStringOrNull(cc, "mode"); + 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); - var connected = getBooleanOrNull(cc, "connected"); - setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_CONNECTED, connected); - setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_MANUAL_MODE_AMP, getDoubleOrNull(cc, "manualmodeamp")); - setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_CURRENT_PWM_AMP, getDoubleOrNull(cc, "currentpwmamp")); - setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_VENDOR, getStringOrNull(cc, "vendor")); - setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_VERSION, getStringOrNull(cc, "version")); + 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(); } - this.parent.onCommunicationFailed(false); - this.parent.onChargeControlStatus(state, stateId, connected); + hb.onCommunicationFailed(false); + hb.onChargeControlStatus(state, stateId, connected); } /** @@ -203,112 +212,36 @@ public void handleMeterResponse(String body) { JsonObject json; try { json = JsonUtils.parseToJsonObject(body); - } catch (Exception e) { + } catch (OpenemsNamedException e) { this.log.warn("Cannot parse ECB1 meter response: " + e.getMessage()); - this.clearMeterValues(); - return; + json = new JsonObject(); } - var meter = JsonUtils.getAsOptionalJsonObject(json, "meter").orElse(null); - if (meter == null) { - this.clearMeterValues(); - return; - } + final var meter = JsonUtils.getAsOptionalJsonObject(json, "meter").orElse(null); + final var data = JsonUtils.getAsOptionalJsonObject(meter, "data").orElse(null); + final var hb = this.parent; - var data = JsonUtils.getAsOptionalJsonObject(meter, "data").orElse(null); - if (data == null) { - this.clearMeterValues(); - return; - } - - setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_SERIAL, getIntOrNull(meter, "serial")); - setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, getStringOrNull(meter, "vendor")); - setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_TYPE, getStringOrNull(meter, "type")); + 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) - var powerTotal = roundToInt(getObisDouble(data, OBIS_POWER_TOTAL)); - var powerL1 = roundToInt(getObisDouble(data, OBIS_POWER_L1)); - var powerL2 = roundToInt(getObisDouble(data, OBIS_POWER_L2)); - var powerL3 = roundToInt(getObisDouble(data, OBIS_POWER_L3)); - - this.parent._setActivePower(powerTotal); - this.parent._setActivePowerL1(powerL1); - this.parent._setActivePowerL2(powerL2); - this.parent._setActivePowerL3(powerL3); + 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) - var currentL1 = roundToInt(scale(getObisDouble(data, OBIS_CURRENT_L1), 1000.0)); - var currentL2 = roundToInt(scale(getObisDouble(data, OBIS_CURRENT_L2), 1000.0)); - var currentL3 = roundToInt(scale(getObisDouble(data, OBIS_CURRENT_L3), 1000.0)); - - this.parent._setCurrentL1(currentL1); - this.parent._setCurrentL2(currentL2); - this.parent._setCurrentL3(currentL3); + 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) - var voltageL1 = roundToInt(scale(getObisDouble(data, OBIS_VOLTAGE_L1), 1000.0)); - var voltageL2 = roundToInt(scale(getObisDouble(data, OBIS_VOLTAGE_L2), 1000.0)); - var voltageL3 = roundToInt(scale(getObisDouble(data, OBIS_VOLTAGE_L3), 1000.0)); - - this.parent._setVoltageL1(voltageL1); - this.parent._setVoltageL2(voltageL2); - this.parent._setVoltageL3(voltageL3); + 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) - var energyWh = getObisDouble(data, OBIS_ENERGY_TOTAL); - Long energyWhLong = energyWh == null ? null : (long) Math.round(energyWh); - this.parent._setActiveProductionEnergy(energyWhLong); - this.parent._setActiveConsumptionEnergy(energyWhLong); - } - - private void clearMeterValues() { - setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_SERIAL, null); - setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, null); - setValue(this.parent, EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_TYPE, null); - - this.parent._setActivePower(null); - this.parent._setActivePowerL1(null); - this.parent._setActivePowerL2(null); - this.parent._setActivePowerL3(null); - this.parent._setCurrentL1(null); - this.parent._setCurrentL2(null); - this.parent._setCurrentL3(null); - this.parent._setVoltageL1(null); - this.parent._setVoltageL2(null); - this.parent._setVoltageL3(null); - this.parent._setActiveProductionEnergy(null); - this.parent._setActiveConsumptionEnergy(null); - } - - // ------------------------------------------------------------------------- - // JSON helpers - // ------------------------------------------------------------------------- - - private static Double getObisDouble(JsonObject data, String obisCode) { - return JsonUtils.getAsOptionalDouble(data, obisCode).orElse(null); - } - - private static Integer roundToInt(Double value) { - return value == null ? null : (int) round(value); - } - - private static Double scale(Double value, double factor) { - return value == null ? null : value * factor; - } - - private static Integer getIntOrNull(JsonObject obj, String key) { - return JsonUtils.getAsOptionalInt(obj, key).orElse(null); - } - - private static String getStringOrNull(JsonObject obj, String key) { - return JsonUtils.getAsOptionalString(obj, key).orElse(null); - } - - private static Boolean getBooleanOrNull(JsonObject obj, String key) { - return JsonUtils.getAsOptionalBoolean(obj, key).orElse(null); - } - - private static Double getDoubleOrNull(JsonObject obj, String key) { - return JsonUtils.getAsOptionalDouble(obj, key).orElse(null); + 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/EvseChargePointHardyBarthEcb1.java b/io.openems.edge.evse.chargepoint.hardybarth/src/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1.java index 64890efecc2..a0f56b40fe2 100644 --- 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 @@ -18,7 +18,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { *

  • 17 = Stopped / paused *
*/ - RAW_STATE_ID(Doc.of(OpenemsType.INTEGER) // + RAW_STATE_ID(Doc.of(OpenemsType.INTEGER)// .text("ECB1 state ID (5=charging, 17=stopped)")), /** @@ -33,45 +33,45 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { *
  • F = Fault * */ - RAW_STATE(Doc.of(OpenemsType.STRING) // + 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) // + 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) // + 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) // + 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) // + RAW_CONNECTED(Doc.of(OpenemsType.BOOLEAN)// .text("Vehicle connected")), /** EVCC vendor name. */ - RAW_VENDOR(Doc.of(OpenemsType.STRING) // + RAW_VENDOR(Doc.of(OpenemsType.STRING)// .text("EVCC vendor")), /** EVCC firmware version. */ - RAW_VERSION(Doc.of(OpenemsType.STRING) // + RAW_VERSION(Doc.of(OpenemsType.STRING)// .text("EVCC firmware version")), /** Meter serial number. */ - RAW_METER_SERIAL(Doc.of(OpenemsType.INTEGER) // + RAW_METER_SERIAL(Doc.of(OpenemsType.INTEGER)// .text("Meter serial number")), /** Meter vendor. */ - RAW_METER_VENDOR(Doc.of(OpenemsType.STRING) // + RAW_METER_VENDOR(Doc.of(OpenemsType.STRING)// .text("Meter vendor")), /** Meter type. */ - RAW_METER_TYPE(Doc.of(OpenemsType.STRING) // + RAW_METER_TYPE(Doc.of(OpenemsType.STRING)// .text("Meter type")); private final Doc doc; 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/ecb1/EvseChargePointHardyBarthEcb1ImplTest.java b/io.openems.edge.evse.chargepoint.hardybarth/test/io/openems/edge/evse/chargepoint/hardybarth/ecb1/EvseChargePointHardyBarthEcb1ImplTest.java index ac5d0f4c77b..f5093f084a8 100644 --- 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 @@ -12,9 +12,11 @@ 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; @@ -184,16 +186,55 @@ void testCharging() throws Exception { 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(EvseChargePointHardyBarthEcb1.ChannelId.RAW_STATE, "C") // + .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.ACTIVE_CONSUMPTION_ENERGY, 10000L) // + .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) // ); } @@ -287,12 +328,9 @@ void testSetCurrentSendsHttpRequests() throws Exception { 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"); + 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 @@ -467,9 +505,7 @@ void testPauseStopsCharging() throws Exception { 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"); + 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"); } } From 23b423a3b9be8916d4116fafd35d8996271e8cb4 Mon Sep 17 00:00:00 2001 From: Stefan Feilmeier Date: Wed, 23 Sep 2026 23:00:26 +0200 Subject: [PATCH 18/19] Checkstyle --- io.openems.common/src/io/openems/common/utils/JsonUtils.java | 4 ++-- .../edge/evse/chargepoint/hardybarth/ecb1/Ecb1Handler.java | 3 --- .../io/openems/edge/evcs/keba/modbus/EvcsKebaModbusImpl.java | 2 -- .../openems/edge/evcs/keba/modbus/EvcsKebaModbusImplTest.java | 1 - 4 files changed, 2 insertions(+), 8 deletions(-) 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 70e4523fc6b..cc824a6e45e 100644 --- a/io.openems.common/src/io/openems/common/utils/JsonUtils.java +++ b/io.openems.common/src/io/openems/common/utils/JsonUtils.java @@ -973,7 +973,7 @@ public static String getAsStringOrElse(JsonElement jElement, String memberName, /** * Gets the member of the {@link JsonElement} as {@link String} if it exists; or - * `null` otherwise + * `null` otherwise. * * @param jElement the {@link JsonElement} * @param memberName the name of the member @@ -1058,7 +1058,7 @@ public static Optional getAsOptionalBoolean(JsonElement jElement, Strin /** * Gets the member of the {@link JsonElement} as {@link Boolean} if it exists; - * or `null` otherwise + * or `null` otherwise. * * @param jElement the {@link JsonElement} * @param memberName the name of the member 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 index aad3cfb1db3..3d66c0440a1 100644 --- 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 @@ -21,9 +21,6 @@ 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.jsonrpc.serialization.JsonObjectPath; -import io.openems.common.jsonrpc.serialization.JsonObjectPathActual; -import io.openems.common.jsonrpc.serialization.JsonObjectPathActual.JsonObjectPathActualNonNull; import io.openems.common.utils.JsonUtils; import io.openems.common.utils.LatestWinsFutureExecutor; import io.openems.edge.bridge.http.cycle.HttpBridgeCycleService; 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; From 214b92ecc6199688040ae00c981d1f4dee4ceb16 Mon Sep 17 00:00:00 2001 From: Stefan Feilmeier Date: Wed, 23 Sep 2026 23:28:57 +0200 Subject: [PATCH 19/19] Fix TypeUtils.multiply --- .../openems/edge/common/type/TypeUtils.java | 20 ++++++++++--------- .../edge/common/type/TextProviderTest.java | 3 ++- .../edge/common/type/TypeUtilsTest.java | 6 +++--- ...EvseChargePointHardyBarthEcb1ImplTest.java | 4 +--- 4 files changed, 17 insertions(+), 16 deletions(-) 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/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 index f5093f084a8..bfb8059724f 100644 --- 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 @@ -246,14 +246,12 @@ void testInvalidMeterResponseClearsPreviousValues() throws Exception { test // .next(new TestCase() // .onBeforeProcessImage(() -> handler.handleMeterResponse(METER_CHARGING)) // - .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, "eCHARGE") // .output(ElectricityMeter.ChannelId.ACTIVE_POWER, 11040) // .output(ElectricityMeter.ChannelId.CURRENT_L1, 16_000) // .output(ElectricityMeter.ChannelId.VOLTAGE_L1, 230_000) // - .output(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY, 10000L)) // + .output(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, 10000L)) // .next(new TestCase() // .onBeforeProcessImage(() -> handler.handleMeterResponse(METER_WITHOUT_DATA)) // - .output(EvseChargePointHardyBarthEcb1.ChannelId.RAW_METER_VENDOR, null) // .output(ElectricityMeter.ChannelId.ACTIVE_POWER, null) // .output(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, null) // .output(ElectricityMeter.ChannelId.CURRENT_L1, null) //