Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2addfcf
[Edge] Add Hardy Barth cPH1 EVCS support via ECB1 REST API
Christoph-87 Sep 2, 2026
fc1a748
[Edge] Fix Hardy Barth cPH1: send empty body on /start and /stop
Christoph-87 Sep 2, 2026
cb22a27
[Edge] Fix Hardy Barth cPH1 ECB1: correct API usages causing CI build…
Christoph-87 Sep 2, 2026
2f1a2eb
[Edge] Fix Hardy Barth cPH1 ECB1: fix Checkstyle VariableDeclarationU…
Christoph-87 Sep 2, 2026
4f38860
[Edge] Fix Hardy Barth cPH1 ECB1: fix Checkstyle distance for connected
Christoph-87 Sep 2, 2026
88bc48b
[Edge] Fix Hardy Barth cPH1: dispatch /mode/manual/ampere and /start …
Christoph-87 Sep 2, 2026
77442af
[Edge] Add Hardy Barth cPH1 EVSE implementation (Evse.ChargePoint.Har…
Christoph-87 Sep 2, 2026
7ebe1d8
[Edge] Fix Hardy Barth cPH1 EVSE: replace channel<Boolean> read with …
Christoph-87 Sep 2, 2026
9b7a9cc
[Edge] Fix Hardy Barth cPH1: make handler test methods public
Christoph-87 Sep 2, 2026
2d448ec
[Edge] Fix Hardy Barth cPH1 EVSE: add missing Javadoc on MyConfig.cre…
Christoph-87 Sep 2, 2026
4c7886a
[Edge] Hardy Barth cPH1 EVSE: add tests for getChargePointAbilities a…
Christoph-87 Sep 2, 2026
bf2a2d7
Merge remote-tracking branch 'origin/develop' into feature/evcs-hardy…
sfeilmeier Sep 3, 2026
c534eb3
Apply tools/prepare-commit.sh
sfeilmeier Sep 3, 2026
fbb0a8a
[Edge] Hardy Barth cPH1: remove EVCS, move Ecb1Handler/Parent to EVSE…
Christoph-87 Sep 3, 2026
b71eb1f
Merge branch 'develop' into pr/3940
sfeilmeier Sep 4, 2026
52d83f0
Merge remote-tracking branch 'origin/develop' into feature/evcs-hardy…
sfeilmeier Sep 19, 2026
88ec6cd
fix(evse): address Hardy Barth cPH1 review
Christoph-87 Sep 20, 2026
ca38332
test(evse): extend Hardy Barth cPH1 coverage
Christoph-87 Sep 21, 2026
3ee6661
Merge remote-tracking branch 'origin/develop' into feature/evcs-hardy…
sfeilmeier Sep 23, 2026
e11baf8
Explicitely name cPH2 package
sfeilmeier Sep 23, 2026
847108a
Improve usage of existing Utils
sfeilmeier Sep 23, 2026
23b423a
Checkstyle
sfeilmeier Sep 23, 2026
214b92e
Fix TypeUtils.multiply
sfeilmeier Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions io.openems.common/src/io/openems/common/utils/JsonUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -1044,6 +1056,18 @@ public static Optional<Boolean> 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.
*
Expand Down Expand Up @@ -1165,6 +1189,18 @@ public static Optional<Integer> 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.
*
Expand Down Expand Up @@ -1322,6 +1358,18 @@ public static Optional<Double> 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}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -308,7 +308,7 @@ public void sumDouble() {
private static void assertException(ThrowingRunnable<Exception> runnable) {
try {
runnable.run();
assertEquals("Expecting an Exception!", true, false);
assertEquals(true, false, "Expecting an Exception!");
} catch (Exception e) {
// ok
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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/<project>=UTF-8
encoding/bnd.bnd=UTF-8
encoding/readme.adoc=UTF-8
69 changes: 65 additions & 4 deletions io.openems.edge.evse.chargepoint.hardybarth/readme.adoc
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
= Hardy Barth Salia Charging Station
= Hardy Barth Charging Stations

Three-phase AC charging station with REST API integration.
Three-phase AC charging stations with REST API integration.

== Overview

This bundle provides integration with the Salia charging station by Hardy Barth, a three-phase AC electric vehicle charging station designed for commercial and residential charging applications. The station communicates via REST API over IP for flexible integration with energy management systems.
This bundle provides integration with Hardy Barth charging stations. Two device families are supported, each using a different REST API:

Hardy Barth Salia is suitable for commercial charging networks, workplace charging, and site energy management requiring three-phase charging integration.
* **Hardy Barth Salia** (`Evcs.HardyBarth`, `Evse.ChargePoint.HardyBarth`) – Salia controller API (`/api/secc`)
* **Hardy Barth cPH1 (ECB1)** (`Evse.ChargePoint.HardyBarth.cPH1`) – eCharge ECB1 REST API (`/api/v1/chargecontrols/`, `/api/v1/meters/`)

== Supported Devices

Expand Down Expand Up @@ -83,4 +84,64 @@ This bundle implements the following OpenEMS Components:

[[_evcs_hardy]]

=== <<_evse_cph1,EVSE Charge-Point Hardy Barth cPH1>>

*Name*: EVSE Charge-Point Hardy Barth cPH1

*Factory-PID*: `Evse.ChargePoint.HardyBarth.cPH1`

.Implemented Natures/Interfaces
* EvseChargePointHardyBarthEcb1
* OpenemsComponent
* EvseChargePoint
* ElectricityMeter

*Description*: EVSE interface for the Hardy Barth eCharge cPH1 wallbox (EVCC Basic controller, ECB1 REST API).
Reads charge-control status and meter data from `/api/v1/chargecontrols/{id}` and `/api/v1/meters/{id}`.
Write operations (start, stop, set current) are dispatched as HTTP POST requests.

.*Configuration*:

* `id` (String): Component ID for unique identification (default: "evseChargePoint0")
* `alias` (String): Human-readable alias for the component
* `enabled` (Boolean): Enable/disable this component
* `ip` (String): IP address of the cPH1 wallbox (required, e.g. "192.168.2.8")
* `chargeControlId` (Integer): Charge-control unit ID (default: 1)
* `meterId` (Integer): Energy-meter ID (default: 1)
* `minHwCurrent` (Integer): Minimum hardware current in mA (default: 6000)
* `maxHwCurrent` (Integer): Maximum hardware current in mA (default: 32000)
* `phaseRotation` (PhaseRotation): Phase wiring configuration (default: L1_L2_L3)
* `readOnly` (Boolean): Read-only mode (monitoring only) vs managed control (default: false)

.ECB1 API Endpoints Used
|===
| Operation | Method | Endpoint

| Read charge-control status
| GET
| `/api/v1/chargecontrols/{id}`

| Read meter data
| GET
| `/api/v1/meters/{id}`

| Set manual mode
| POST
| `/api/v1/chargecontrols/{id}/mode` (`mode=manual`)

| Start charging
| POST
| `/api/v1/chargecontrols/{id}/start`

| Stop charging
| POST
| `/api/v1/chargecontrols/{id}/stop`

| Set current
| POST
| `/api/v1/chargecontrols/{id}/mode/manual/ampere` (`manualmodeamp={A}`)
|===

[[_evse_cph1]]

https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.evse.chargepoint.hardybarth[Source Code icon:github[]]
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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), //
Expand Down Expand Up @@ -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"), //

Expand Down Expand Up @@ -255,6 +252,7 @@ public default StringReadChannel getRawDeviceModelNameChannel() {

/**
* get Salia Device Model Name.
*
* @return channel value
*/
public default String getSaliaDeviceModelName() {
Expand All @@ -263,6 +261,7 @@ public default String getSaliaDeviceModelName() {

/**
* get Raw Device Product Channel.
*
* @return the channel
*/
public default StringReadChannel getRawDeviceProductChannel() {
Expand All @@ -271,6 +270,7 @@ public default StringReadChannel getRawDeviceProductChannel() {

/**
* get raw device product.
*
* @return channel value
*/
public default String getRawDeviceProduct() {
Expand All @@ -281,8 +281,8 @@ public default String getRawDeviceProduct() {
* Reports whether the phase-switching API is available.
*
* <p>
* 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:
* <ul>
* <li>{@code "idle"} and {@code "progress"} indicate support.
* <li>A missing, null, undefined or any unknown value indicates no support.
Expand Down Expand Up @@ -310,16 +310,15 @@ public default boolean hasPhaseSwitchingApi() {
* Reports whether the phase-switching API is currently idle.
*
* <p>
* 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.
*
* <p>
* 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"}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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;

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")
Expand All @@ -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}]";
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading
Loading