diff --git a/bundles/org.openhab.binding.evcc/README.md b/bundles/org.openhab.binding.evcc/README.md index bb9615a791..070bf0f890 100644 --- a/bundles/org.openhab.binding.evcc/README.md +++ b/bundles/org.openhab.binding.evcc/README.md @@ -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. diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/EvccBindingConstants.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/EvccBindingConstants.java index 33b8072e54..138d0ea346 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/EvccBindingConstants.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/EvccBindingConstants.java @@ -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"; diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/EvccDiscoveryService.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/EvccDiscoveryService.java index a32215486b..b7f45a4167 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/EvccDiscoveryService.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/EvccDiscoveryService.java @@ -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 { - 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); + } } } } diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/Utils.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/Utils.java index d1aa8b6613..f537ed8a0b 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/Utils.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/Utils.java @@ -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 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)); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/BatteryDiscoveryMapper.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/BatteryDiscoveryMapper.java index 20fbd141a7..5dd59ed4a5 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/BatteryDiscoveryMapper.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/BatteryDiscoveryMapper.java @@ -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 discover(JsonObject state, EvccBridgeHandler bridgeHandler) { - List 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 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)); } } diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/ForecastDiscoveryMapper.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/ForecastDiscoveryMapper.java index 8f7e8f06d3..b7c9047281 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/ForecastDiscoveryMapper.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/ForecastDiscoveryMapper.java @@ -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) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/PlanDiscoveryMapper.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/PlanDiscoveryMapper.java index 4bdeb07d9e..525655ce89 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/PlanDiscoveryMapper.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/PlanDiscoveryMapper.java @@ -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; diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBatteryHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBatteryHandler.java index be377c7f96..be04831d8c 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBatteryHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBatteryHandler.java @@ -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(); } } diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandler.java index c8c45eb6de..d0a29de07e 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandler.java @@ -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 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 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); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandler.java index e835556d25..58586a322e 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandler.java @@ -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); } diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBatteryHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBatteryHandlerTest.java index 255fd1529e..e7047073a3 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBatteryHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBatteryHandlerTest.java @@ -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