[evcc] Battery values are not shown anymore (#20309)

* [evcc] syntactic simplification (#20246)

Signed-off-by: Maik Scheibler <maik@scheibler-family.de>
This commit is contained in:
Maik Scheibler
2026-03-06 10:32:11 +01:00
committed by GitHub
parent b1ac37129e
commit 4f99334ab3
14 changed files with 2871 additions and 1427 deletions
+3 -2
View File
@@ -1,9 +1,10 @@
# evcc Binding
This binding integrates [evcc](https://evcc.io), an extensible **E**lectric **V**ehicle **C**harge **C**ontroller and home energy management system.
The binding is compatible to evcc [version 0.123.1](https://github.com/evcc-io/evcc/releases/tag/0.123.1) or newer and was tested with [version 0.205.0](https://github.com/evcc-io/evcc/releases/tag/0.205.0).
The binding is compatible to evcc [version 0.209.8](https://github.com/evcc-io/evcc/releases/tag/0.209.8) or newer and was tested with [version 0.301.1](https://github.com/evcc-io/evcc/releases/tag/0.301.1).
You can easily install and upgrade evcc on openHABian using `sudo openhabian-config`.
**Important compatibility note:** Starting with this version of the binding, evcc versions **below 0.209.8** are no longer supported due to changes in the evcc API.
If you are currently running evcc `< 0.209.8`, you must either upgrade your evcc installation to at least `0.209.8` or continue using an older version of this binding that still supports the legacy API.
evcc controls your wallbox(es) with multiple charging modes and allows you to charge your ev with your photovoltaic's excess current.
To provide an intelligent charging control, evcc supports over 30 wallboxes and over 20 energy meters/home energy management systems from many manufacturers as well as electric vehicles from over 20 car manufacturers.
@@ -76,6 +76,7 @@ public class EvccBindingConstants {
// JSON Keys
public static final String JSON_KEY_ACTIVE = "active";
public static final String JSON_KEY_BATTERY = "battery";
public static final String JSON_KEY_DEVICES = "devices";
public static final String JSON_KEY_CHARGE_CURRENT = "chargeCurrent";
public static final String JSON_KEY_CHARGE_CURRENTS = "chargeCurrents";
public static final String JSON_KEY_CHARGE_VOLTAGES = "chargeVoltages";
@@ -12,7 +12,7 @@
*/
package org.openhab.binding.evcc.internal.discovery;
import static org.openhab.binding.evcc.internal.EvccBindingConstants.*;
import static org.openhab.binding.evcc.internal.EvccBindingConstants.SUPPORTED_THING_TYPES;
import java.util.List;
import java.util.Optional;
@@ -37,7 +37,7 @@ import com.google.gson.JsonObject;
/**
* The {@link EvccDiscoveryService} is responsible for scanning the API response for things
*
*
* @author Marcel Goerentz - Initial contribution
*/
@NonNullByDefault
@@ -66,10 +66,15 @@ public class EvccDiscoveryService extends AbstractThingHandlerDiscoveryService<E
JsonObject state = thingHandler.getCachedEvccState().deepCopy();
if (!state.isEmpty()) {
for (EvccDiscoveryMapper mapper : mappers) {
mapper.discover(state, thingHandler).forEach(thing -> {
logger.debug("Thing discovered {}", thing);
thingDiscovered(thing);
});
try {
mapper.discover(state, thingHandler).forEach(thing -> {
logger.debug("Thing discovered {}", thing);
thingDiscovered(thing);
});
} catch (RuntimeException e) {
// isolate the mapper calls so that a single evcc API change does not affect stable parts
logger.warn("discovery for things failed, possibly due to an API change", e);
}
}
}
}
@@ -16,7 +16,6 @@ import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.Normalizer;
import java.util.List;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -32,7 +31,7 @@ public class Utils {
/**
* This method removes specific local characters (like ä, ö, ü), so we get a sanitized string
*
*
* @param name that will be sanitized
* @return a sanitized name that has replaced any invalid char
*/
@@ -49,10 +48,10 @@ public class Utils {
* This method creates a stable ID string based on the provided list of values (cut down to first 10 hex chars of
* SHA-256)
*
* @param values list of strings to create the ID from
* @param values strings to create the ID from
* @return a stable ID string
*/
public static String createIdString(List<String> values) {
public static String createIdString(String... values) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(String.join("", values).getBytes(StandardCharsets.UTF_8));
@@ -14,10 +14,11 @@ package org.openhab.binding.evcc.internal.discovery.mapper;
import static org.openhab.binding.evcc.internal.EvccBindingConstants.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.StreamSupport;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.evcc.internal.EvccBindingConstants;
@@ -29,10 +30,11 @@ import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* The {@link BatteryDiscoveryMapper} iis responsible for mapping the discovered batteries to discovery results
* The {@link BatteryDiscoveryMapper} is responsible for mapping the discovered batteries to discovery results.
*
* @author Marcel Goerentz - Initial contribution
*/
@@ -42,24 +44,28 @@ public class BatteryDiscoveryMapper implements EvccDiscoveryMapper {
@Override
public Collection<DiscoveryResult> discover(JsonObject state, EvccBridgeHandler bridgeHandler) {
List<DiscoveryResult> results = new ArrayList<>();
JsonArray batteries = state.getAsJsonArray(JSON_KEY_BATTERY);
if (batteries == null) {
return results;
}
for (int i = 0; i < batteries.size(); i++) {
JsonObject battery = batteries.get(i).getAsJsonObject();
String title = battery.has(JSON_KEY_TITLE)
? battery.get(JSON_KEY_TITLE).getAsString().toLowerCase(Locale.ROOT)
: JSON_KEY_BATTERY + i;
AtomicInteger counter = new AtomicInteger(0);
return getBatteryArray(state).stream()
.flatMap(deviceArray -> StreamSupport.stream(deviceArray.spliterator(), false))
.filter(JsonElement::isJsonObject).map(JsonElement::getAsJsonObject).map(battery -> {
int index = counter.getAndIncrement();
String title = battery.has(JSON_KEY_TITLE)
? battery.get(JSON_KEY_TITLE).getAsString().toLowerCase(Locale.ROOT)
: JSON_KEY_BATTERY + index;
ThingUID uid = new ThingUID(EvccBindingConstants.THING_TYPE_BATTERY, bridgeHandler.getThing().getUID(),
Utils.sanitizeName(title));
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(title)
.withBridge(bridgeHandler.getThing().getUID()).withProperty(PROPERTY_INDEX, i)
.withProperty(PROPERTY_TITLE, title).withRepresentationProperty(PROPERTY_TITLE).build();
results.add(result);
}
return results;
ThingUID uid = new ThingUID(EvccBindingConstants.THING_TYPE_BATTERY,
bridgeHandler.getThing().getUID(), Utils.sanitizeName(title));
return DiscoveryResultBuilder.create(uid).withLabel(title)
.withBridge(bridgeHandler.getThing().getUID()).withProperty(PROPERTY_INDEX, index)
.withProperty(PROPERTY_TITLE, title).withRepresentationProperty(PROPERTY_TITLE).build();
}).toList();
}
private Optional<JsonArray> getBatteryArray(JsonObject state) {
return Optional.ofNullable(state.get(JSON_KEY_BATTERY)).map(battElement -> battElement.isJsonArray()
// for up to version 0.300.0
? (JsonArray) battElement
// for version 0.300.0+
: ((JsonObject) battElement).getAsJsonArray(JSON_KEY_DEVICES));
}
}
@@ -55,7 +55,7 @@ public class ForecastDiscoveryMapper implements EvccDiscoveryMapper {
ThingUID uid = new ThingUID(THING_TYPE_FORECAST, bridgeHandler.getThing().getUID(),
Utils.sanitizeName(forecastType));
String label = "Forecast " + capitalizeFirstLetter(forecastType);
String id = Utils.createIdString(List.of(label));
String id = Utils.createIdString(label);
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(label)
.withBridge(bridgeHandler.getThing().getUID()).withProperty(PROPERTY_TYPE, PROPERTY_FORECAST)
.withProperty(PROPERTY_SUBTYPE, forecastType).withProperty(PROPERTY_ID, id)
@@ -83,16 +83,16 @@ public class PlanDiscoveryMapper implements EvccDiscoveryMapper {
String localizedLabel = tp.getText(ctx.getBundle(), "discovery.evcc.plan.one-time.label",
"One-time charging plan for {0}", lp.getLocale(), title);
String label = localizedLabel == null ? "One-time charging plan for " + title : localizedLabel;
results.add(createPlanDiscoveryResult(label, Utils.createIdString(List.of(id, "Plan", String.valueOf(0))),
0, id, bridgeHandler));
results.add(createPlanDiscoveryResult(label, Utils.createIdString(id, "Plan", String.valueOf(0)), 0, id,
bridgeHandler));
}
if (vehicle.has(JSON_KEY_REPEATING_PLANS) && vehicle.get(JSON_KEY_REPEATING_PLANS).isJsonArray()) {
for (int index = 1; index <= vehicle.get(JSON_KEY_REPEATING_PLANS).getAsJsonArray().size(); index++) {
String localizedLabel = tp.getText(ctx.getBundle(), "discovery.evcc.plan.repeating.label",
"Repeating plan {0} for {1}", lp.getLocale(), index, title);
String label = localizedLabel == null ? "Repeating plan " + index + " for " + title : localizedLabel;
results.add(createPlanDiscoveryResult(label,
Utils.createIdString(List.of(id, "Plan", String.valueOf(index))), index, id, bridgeHandler));
results.add(createPlanDiscoveryResult(label, Utils.createIdString(id, "Plan", String.valueOf(index)),
index, id, bridgeHandler));
}
}
return results;
@@ -22,6 +22,8 @@ import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
@@ -50,20 +52,27 @@ public class EvccBatteryHandler extends EvccBaseThingHandler {
return;
}
JsonObject state = stateOpt.getAsJsonArray(JSON_KEY_BATTERY).get(index).getAsJsonObject();
JsonObject state = getStateFromCachedState(stateOpt);
commonInitialize(state);
});
}
@Override
public void prepareApiResponseForChannelStateUpdate(JsonObject state) {
state = state.has(JSON_KEY_BATTERY) ? state.getAsJsonArray(JSON_KEY_BATTERY).get(index).getAsJsonObject()
state = state.has(JSON_KEY_BATTERY) && state.getAsJsonObject(JSON_KEY_BATTERY).has(JSON_KEY_DEVICES)
? getStateFromCachedState(state)
: new JsonObject();
updateStatesFromApiResponse(state);
}
@Override
public JsonObject getStateFromCachedState(JsonObject state) {
return state.getAsJsonArray(JSON_KEY_BATTERY).get(index).getAsJsonObject();
JsonElement battElement = state.get(JSON_KEY_BATTERY);
JsonArray battArray = battElement.isJsonArray()
// for up to version 0.300.0
? (JsonArray) battElement
// for version 0.300.0+
: ((JsonObject) battElement).getAsJsonArray(JSON_KEY_DEVICES);
return battArray.get(index).getAsJsonObject();
}
}
@@ -15,6 +15,8 @@ package org.openhab.binding.evcc.internal.handler;
import static org.openhab.binding.evcc.internal.EvccBindingConstants.*;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -37,12 +39,13 @@ public class EvccHeatingHandler extends EvccLoadpointHandler {
private final Logger logger = LoggerFactory.getLogger(EvccHeatingHandler.class);
private static final Map<String, String> JSON_KEYS = Map.ofEntries(
Map.entry("effectiveLimitTemperature", JSON_KEY_EFFECTIVE_LIMIT_SOC),
Map.entry("effectivePlanTemperature", JSON_KEY_EFFECTIVE_PLAN_SOC),
Map.entry("limitTemperature", JSON_KEY_LIMIT_SOC),
Map.entry("vehicleLimitTemperature", JSON_KEY_VEHICLE_LIMIT_SOC),
Map.entry("vehicleTemperature", JSON_KEY_VEHICLE_SOC));
// SortedMap to have a stable replacement result, the unit test is relying on it
private static final SortedMap<String, String> JSON_KEYS = new TreeMap<>(
Map.ofEntries(Map.entry("effectiveLimitTemperature", JSON_KEY_EFFECTIVE_LIMIT_SOC),
Map.entry("effectivePlanTemperature", JSON_KEY_EFFECTIVE_PLAN_SOC),
Map.entry("limitTemperature", JSON_KEY_LIMIT_SOC),
Map.entry("vehicleLimitTemperature", JSON_KEY_VEHICLE_LIMIT_SOC),
Map.entry("vehicleTemperature", JSON_KEY_VEHICLE_SOC)));
public EvccHeatingHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) {
super(thing, channelTypeRegistry);
@@ -116,12 +116,6 @@ public class EvccSiteHandler extends EvccBaseThingHandler {
state.add(JSON_KEY_GRID + Utils.capitalizeFirstLetter(entry.getKey()), entry.getValue());
}
}
// Backwards compatibility for evcc 0.209.8 and older
JsonElement startup = state.get("startup");
if (null != startup && startup.isJsonPrimitive() && startup.getAsJsonPrimitive().isBoolean()) {
state.add("startupComplete", startup);
state.remove("startup");
}
state.remove(JSON_KEY_GRID);
state.remove(JSON_KEY_GRID_CONFIGURED);
}
@@ -12,7 +12,7 @@
*/
package org.openhab.binding.evcc.internal.handler;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
@@ -93,7 +93,7 @@ public class EvccBatteryHandlerTest extends AbstractThingHandlerTestClass<EvccBa
EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class);
handler.bridgeHandler = bridgeHandler;
when(bridgeHandler.getCachedEvccState()).thenReturn(exampleResponse);
batteryState = exampleResponse.getAsJsonArray("battery").get(0).getAsJsonObject();
batteryState = exampleResponse.getAsJsonObject("battery").getAsJsonArray("devices").get(0).getAsJsonObject();
}
@SuppressWarnings("null")
@@ -101,11 +101,12 @@ public class EvccHeatingHandlerTest extends AbstractThingHandlerTestClass<EvccHe
heatingObject.remove(JSON_KEY_LIMIT_SOC);
heatingObject.remove(JSON_KEY_EFFECTIVE_PLAN_SOC);
heatingObject.remove(JSON_KEY_VEHICLE_LIMIT_SOC);
// prepare expectation with alphabetically sorted keys, the handler is doing the same ordering
heatingObject.addProperty("effectiveLimitTemperature", 80);
heatingObject.addProperty("vehicleTemperature", 48.3);
heatingObject.addProperty("limitTemperature", 80);
heatingObject.addProperty("effectivePlanTemperature", 20);
heatingObject.addProperty("limitTemperature", 80);
heatingObject.addProperty("vehicleLimitTemperature", 80);
heatingObject.addProperty("vehicleTemperature", 48.3);
}
@SuppressWarnings("null")
@@ -14,9 +14,7 @@ package org.openhab.binding.evcc.internal.handler;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.Map;
@@ -98,10 +96,8 @@ public class EvccSiteHandlerTest extends AbstractThingHandlerTestClass<EvccSiteH
modifiedVerifyObject.addProperty("gridVoltageL1", 230.0);
modifiedVerifyObject.addProperty("gridVoltageL2", 231.0);
modifiedVerifyObject.addProperty("gridVoltageL3", 229.0);
modifiedVerifyObject.addProperty("startupComplete", verifyObject.get("startup").getAsBoolean());
modifiedVerifyObject.remove("gridConfigured");
modifiedVerifyObject.remove("grid");
modifiedVerifyObject.remove("startup");
gridConfigured.addProperty("power", 2000);
gridConfigured.addProperty("energy", 10000);
File diff suppressed because it is too large Load Diff