From b6a3b5e914c3aa3daeb0c8c68f4110c0271e1ff2 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Sat, 23 Aug 2025 22:18:06 +0200 Subject: [PATCH 01/29] Add channels for grid currents, grid energy and charging currents. Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../handler/EvccBaseThingHandler.java | 2 + .../handler/EvccLoadpointHandler.java | 36 +++++++------ .../internal/handler/EvccSiteHandler.java | 15 +++++- .../binding/evcc/internal/handler/Utils.java | 50 +++++++++++++++++++ .../resources/OH-INF/i18n/evcc.properties | 16 +++++- .../main/resources/OH-INF/thing/heating.xml | 2 +- .../main/resources/OH-INF/thing/loadpoint.xml | 30 +++++++++++ .../src/main/resources/OH-INF/thing/site.xml | 44 ++++++++++++++++ 8 files changed, 177 insertions(+), 18 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java index f2fb22fe9eca4..7540a8f3347fd 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java @@ -87,6 +87,7 @@ public EvccBaseThingHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry } protected void commonInitialize(JsonObject state) { + Utils.sortJsonInPlace(state); ThingBuilder builder = editThing(); for (Map.Entry<@Nullable String, @Nullable JsonElement> entry : state.entrySet()) { @@ -255,6 +256,7 @@ public void updateFromEvccState(JsonObject state) { return; } + Utils.sortJsonInPlace(state); for (Map.Entry<@Nullable String, @Nullable JsonElement> entry : state.entrySet()) { String key = entry.getKey(); JsonElement value = entry.getValue(); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java index c554bc5cf9adb..070dab35b71b0 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java @@ -29,6 +29,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; /** @@ -40,6 +41,11 @@ public class EvccLoadpointHandler extends EvccBaseThingHandler { private final Logger logger = LoggerFactory.getLogger(EvccLoadpointHandler.class); + + // JSON keys that need a special treatment, in example for backwards compatibility + private static final Map JSON_KEYS = Map.ofEntries(Map.entry("chargeCurrent", "offeredCurrent"), + Map.entry("vehiclePresent", "connected"), Map.entry("enabled", "charging"), + Map.entry("phases", "phasesConfigured"), Map.entry("chargeCurrents", "")); protected final int index; private int[] version = {}; @@ -76,7 +82,7 @@ public void initialize() { public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof State) { String datapoint = Utils.getKeyFromChannelUID(channelUID).toLowerCase(); - // Backwardscompatibility for phasesConfigured + // Backwards compatibility for phasesConfigured if ("configuredPhases".equals(datapoint) && version[0] == 0 && version[1] < 200) { datapoint = "phases"; } @@ -112,20 +118,20 @@ public void updateFromEvccState(JsonObject state) { } private void modifyJSON(JsonObject state) { - // This is for backward compatibility with older evcc versions - if (state.has("chargeCurrent")) { - state.addProperty("offeredCurrent", state.get("chargeCurrent").getAsDouble()); - state.remove("chargeCurrent"); - } - if (state.has("vehiclePresent")) { - state.add("connected", state.get("vehiclePresent")); - } - if (state.has("enabled")) { - state.add("charging", state.get("enabled")); - } - if (state.has("phases")) { - state.add("phasesConfigured", state.get("phases")); - } + JSON_KEYS.forEach((oldKey, newKey) -> { + if (state.has(oldKey)) { + if (oldKey.equals("chargeCurrents")) { + int phase = 1; + for (JsonElement current : state.getAsJsonArray(oldKey)) { + state.add("phase-" + phase, current); + phase++; + } + } else { + state.add(newKey, state.get(oldKey)); + } + state.remove(oldKey); + } + }); } @Override 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 0a6110d8daaef..fee962122c9d9 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 @@ -12,6 +12,7 @@ */ package org.openhab.binding.evcc.internal.handler; +import java.util.Map; import java.util.Optional; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -26,6 +27,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; /** @@ -95,7 +97,18 @@ public void initialize() { } private void modifyJSON(JsonObject state) { - state.add("gridPower", state.getAsJsonObject("grid").get("power")); + for (Map.Entry entry : state.getAsJsonObject("grid").entrySet()) { + if (entry.getKey().equals("currents")) { + int phase = 1; + for (JsonElement value : entry.getValue().getAsJsonArray()) { + state.add("gridCurrent-" + phase, value); + phase++; + } + } else { + state.add("grid" + Utils.capitalizeFirstLetter(entry.getKey()), entry.getValue()); + } + } + state.remove("grid"); state.remove("gridConfigured"); } diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java index aa1e68d10bfa2..00b2d08e11392 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Objects; import java.util.StringJoiner; +import java.util.TreeMap; import javax.measure.Unit; @@ -32,6 +33,9 @@ import org.openhab.core.library.unit.Units; import org.openhab.core.thing.ChannelUID; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + /** * The {@link Utils} provides utility methods * @@ -122,4 +126,50 @@ public static int[] convertVersionStringToIntArray(String input) { } return Arrays.stream(input.split("\\.")).mapToInt(Integer::parseInt).toArray(); } + + /** + * Recursively sorts all JsonObjects in-place by their keys in alphabetical order. + * This method modifies the original JsonElement structure directly. + * It traverses through all nested JsonObjects and JsonArrays, and for each JsonObject, + * it reorders its keys alphabetically using a TreeMap. + * + * @param element The JsonElement to be sorted. Can be a JsonObject, JsonArray, or primitive. + */ + public static void sortJsonInPlace(JsonElement element) { + if (element.isJsonObject()) { + JsonObject obj = element.getAsJsonObject(); + + // Collect and sort entries by key + TreeMap sortedMap = new TreeMap<>(); + for (Map.Entry entry : obj.entrySet()) { + sortJsonInPlace(entry.getValue()); // Recursively sort child elements + sortedMap.put(entry.getKey(), entry.getValue()); + } + + // Clear and reinsert sorted entries + obj.entrySet().clear(); + for (Map.Entry entry : sortedMap.entrySet()) { + obj.add(entry.getKey(), entry.getValue()); + } + + } else if (element.isJsonArray()) { + for (JsonElement item : element.getAsJsonArray()) { + sortJsonInPlace(item); // Recursively sort array elements + } + } + // Primitive values remain unchanged + } + + /** + * Capitalizes the first character of a string. + * + * @param input The input string. + * @return The string with the first character in uppercase. + */ + public static String capitalizeFirstLetter(String input) { + if (input.isEmpty()) { + return input; + } + return input.substring(0, 1).toUpperCase() + input.substring(1); + } } diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties index c311b301f9721..79ec2bad7e244 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties @@ -161,6 +161,12 @@ channel-type.evcc.loadpoint-mode.state.option.minpv = Min + PV channel-type.evcc.loadpoint-mode.state.option.pv = Only PV channel-type.evcc.loadpoint-offered-current.label = Offered Current channel-type.evcc.loadpoint-offered-current.description = Current amperage per connected phase offered to the vehicle +channel-type.evcc.loadpoint-phase-1.label = Charge Current 1 +channel-type.evcc.loadpoint-phase-1.description = Actual charge current of charger phase 1 +channel-type.evcc.loadpoint-phase-2.label = Charge Current 2 +channel-type.evcc.loadpoint-phase-2.description = Actual charge current of charger phase 2 +channel-type.evcc.loadpoint-phase-3.label = Charge Current 3 +channel-type.evcc.loadpoint-phase-3.description = Actual charge current of charger phase 3 channel-type.evcc.loadpoint-phase-action.label = Phase Scaling channel-type.evcc.loadpoint-phase-action.description = Indicates phase switching from 1 to 3 or 3 to 1 phases channel-type.evcc.loadpoint-phase-action.state.option.scale1p = Switching to 1 phase @@ -240,7 +246,7 @@ channel-type.evcc.loadpoint-vehicle-range.label = Vehicle Range channel-type.evcc.loadpoint-vehicle-range.description = Current range of the vehicle channel-type.evcc.loadpoint-vehicle-soc.label = Vehicle API SoC channel-type.evcc.loadpoint-vehicle-soc.description = Current state of charge of the vehicle -channel-type.evcc.loadpoint-vehicle-temperature.label = Temperature +channel-type.evcc.loadpoint-vehicle-temperature.label = Device Temperature channel-type.evcc.loadpoint-vehicle-temperature.description = Current temperature of the heating device channel-type.evcc.loadpoint-vehicle-title.label = Vehicle Title channel-type.evcc.loadpoint-vehicle-title.description = The name of the vehicle as displayed in the UI @@ -304,6 +310,14 @@ channel-type.evcc.site-green-share-home.label = Green Share Home channel-type.evcc.site-green-share-home.description = Indicates whether green power is available for home channel-type.evcc.site-green-share-loadpoints.label = Green Share Loadpoints channel-type.evcc.site-green-share-loadpoints.description = Indicates whether green power is available for loadpoints +channel-type.evcc.site-grid-current-1.label = Grid Current 1 +channel-type.evcc.site-grid-current-1.description = Grid phase 1 current +channel-type.evcc.site-grid-current-2.label = Grid Current 2 +channel-type.evcc.site-grid-current-2.description = Grid phase 2 current +channel-type.evcc.site-grid-current-3.label = Grid Current 3 +channel-type.evcc.site-grid-current-3.description = Grid phase 3 current +channel-type.evcc.site-grid-energy.label = Grid Energy +channel-type.evcc.site-grid-energy.description = Consumed energy from the grid channel-type.evcc.site-grid-power.label = Grid Power channel-type.evcc.site-grid-power.description = Current power from grid (negative means feed-in) channel-type.evcc.site-home-power.label = Home Power diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/heating.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/heating.xml index ba82a73040d0e..81c94bb9b0d0d 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/heating.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/heating.xml @@ -58,7 +58,7 @@ Number:Temperature - + Current temperature of the heating device Temperature diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml index a8bd1c4166018..962a3483c2b29 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml @@ -429,6 +429,36 @@ + + Number:ElectricCurrent + + Actual charge current of charger phase 1 + + Status + Info + + + + + Number:ElectricCurrent + + Actual charge current of charger phase 2 + + Status + Info + + + + + Number:ElectricCurrent + + Actual charge current of charger phase 3 + + Status + Info + + + Number diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml index bb3f40cb3c1da..896ac6db6cbb9 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml @@ -195,6 +195,50 @@ Indicates whether green power is available for loadpoints + + Number:ElectricCurrent + + Grid phase 1 current + Energy + + Measurement + Current + + + + + Number:ElectricCurrent + + Grid phase 2 current + Energy + + Measurement + Current + + + + + Number:ElectricCurrent + + Grid phase 3 current + Energy + + Measurement + Current + + + + + Number:Energy + + Consumed energy from the grid + Energy + + Measurement + Energy + + + Number:Power From 7cda048db315d504b9a2e00fc0a9974acc9addb7 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Sat, 23 Aug 2025 23:07:03 +0200 Subject: [PATCH 02/29] Correct IDs Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../handler/EvccLoadpointHandler.java | 2 +- .../internal/handler/EvccSiteHandler.java | 2 +- .../resources/OH-INF/i18n/evcc.properties | 29 ++++++++----------- .../main/resources/OH-INF/thing/loadpoint.xml | 12 ++++---- .../src/main/resources/OH-INF/thing/site.xml | 6 ++-- 5 files changed, 23 insertions(+), 28 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java index 070dab35b71b0..fefe8801916ac 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java @@ -123,7 +123,7 @@ private void modifyJSON(JsonObject state) { if (oldKey.equals("chargeCurrents")) { int phase = 1; for (JsonElement current : state.getAsJsonArray(oldKey)) { - state.add("phase-" + phase, current); + state.add("chargeCurrentL" + phase, current); phase++; } } else { 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 fee962122c9d9..7a03519ab523b 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 @@ -101,7 +101,7 @@ private void modifyJSON(JsonObject state) { if (entry.getKey().equals("currents")) { int phase = 1; for (JsonElement value : entry.getValue().getAsJsonArray()) { - state.add("gridCurrent-" + phase, value); + state.add("gridCurrentL" + phase, value); phase++; } } else { diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties index 79ec2bad7e244..8845459e7d15b 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties @@ -68,6 +68,12 @@ channel-type.evcc.loadpoint-battery-boost.label = Battery Boost channel-type.evcc.loadpoint-battery-boost.description = Controls and indicates whether battery boost is active or not channel-type.evcc.loadpoint-battery-boost.state.option.ON = Activated channel-type.evcc.loadpoint-battery-boost.state.option.OFF = Deactivated +channel-type.evcc.loadpoint-charge-current-l1.label = Charge Current 1 +channel-type.evcc.loadpoint-charge-current-l1.description = Actual charge current of charger phase L1 +channel-type.evcc.loadpoint-charge-current-l2.label = Charge Current 2 +channel-type.evcc.loadpoint-charge-current-l2.description = Actual charge current of charger phase L2 +channel-type.evcc.loadpoint-charge-current-l3.label = Charge Current 3 +channel-type.evcc.loadpoint-charge-current-l3.description = Actual charge current of charger phase L3 channel-type.evcc.loadpoint-charge-duration.label = Charging Duration channel-type.evcc.loadpoint-charge-duration.description = Time passed since start of the charging channel-type.evcc.loadpoint-charge-power.label = Charging Power @@ -161,12 +167,6 @@ channel-type.evcc.loadpoint-mode.state.option.minpv = Min + PV channel-type.evcc.loadpoint-mode.state.option.pv = Only PV channel-type.evcc.loadpoint-offered-current.label = Offered Current channel-type.evcc.loadpoint-offered-current.description = Current amperage per connected phase offered to the vehicle -channel-type.evcc.loadpoint-phase-1.label = Charge Current 1 -channel-type.evcc.loadpoint-phase-1.description = Actual charge current of charger phase 1 -channel-type.evcc.loadpoint-phase-2.label = Charge Current 2 -channel-type.evcc.loadpoint-phase-2.description = Actual charge current of charger phase 2 -channel-type.evcc.loadpoint-phase-3.label = Charge Current 3 -channel-type.evcc.loadpoint-phase-3.description = Actual charge current of charger phase 3 channel-type.evcc.loadpoint-phase-action.label = Phase Scaling channel-type.evcc.loadpoint-phase-action.description = Indicates phase switching from 1 to 3 or 3 to 1 phases channel-type.evcc.loadpoint-phase-action.state.option.scale1p = Switching to 1 phase @@ -310,12 +310,12 @@ channel-type.evcc.site-green-share-home.label = Green Share Home channel-type.evcc.site-green-share-home.description = Indicates whether green power is available for home channel-type.evcc.site-green-share-loadpoints.label = Green Share Loadpoints channel-type.evcc.site-green-share-loadpoints.description = Indicates whether green power is available for loadpoints -channel-type.evcc.site-grid-current-1.label = Grid Current 1 -channel-type.evcc.site-grid-current-1.description = Grid phase 1 current -channel-type.evcc.site-grid-current-2.label = Grid Current 2 -channel-type.evcc.site-grid-current-2.description = Grid phase 2 current -channel-type.evcc.site-grid-current-3.label = Grid Current 3 -channel-type.evcc.site-grid-current-3.description = Grid phase 3 current +channel-type.evcc.site-grid-current-l1.label = Grid Current 1 +channel-type.evcc.site-grid-current-l1.description = Grid phase 1 current +channel-type.evcc.site-grid-current-l2.label = Grid Current 2 +channel-type.evcc.site-grid-current-l2.description = Grid phase 2 current +channel-type.evcc.site-grid-current-l3.label = Grid Current 3 +channel-type.evcc.site-grid-current-l3.description = Grid phase 3 current channel-type.evcc.site-grid-energy.label = Grid Energy channel-type.evcc.site-grid-energy.description = Consumed energy from the grid channel-type.evcc.site-grid-power.label = Grid Power @@ -401,8 +401,3 @@ thing-type.config.evcc.server.schema.label = Protocol schema thing-type.config.evcc.server.schema.description = The protocol schema that should be used to connect to your instance thing-type.config.evcc.server.schema.option.http = HTTP thing-type.config.evcc.server.schema.option.https = HTTPS - -# channel types - -channel-type.evcc.site-battery-grid-charge-limit.label = Grid Charging Limit -channel-type.evcc.site-battery-grid-charge-limit.description = Below this smart cost limit the charging from grid will start (currency or co2) diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml index 962a3483c2b29..ccc699de232c1 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml @@ -429,30 +429,30 @@ - + Number:ElectricCurrent - Actual charge current of charger phase 1 + Actual charge current of charger phase L1 Status Info - + Number:ElectricCurrent - Actual charge current of charger phase 2 + Actual charge current of charger phase L2 Status Info - + Number:ElectricCurrent - Actual charge current of charger phase 3 + Actual charge current of charger phase L3 Status Info diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml index 896ac6db6cbb9..4be27b8a38564 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml @@ -195,7 +195,7 @@ Indicates whether green power is available for loadpoints - + Number:ElectricCurrent Grid phase 1 current @@ -206,7 +206,7 @@ - + Number:ElectricCurrent Grid phase 2 current @@ -217,7 +217,7 @@ - + Number:ElectricCurrent Grid phase 3 current From c06cb345ea6fabfe32f6a1834bdaf3aaa14911e6 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:46:53 +0200 Subject: [PATCH 03/29] First unit tests Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../discovery/EvccDiscoveryService.java | 2 +- .../handler/EvccBaseThingHandler.java | 9 +- .../internal/handler/EvccBatteryHandler.java | 6 +- .../internal/handler/EvccBridgeHandler.java | 4 +- .../internal/handler/EvccHeatingHandler.java | 4 +- .../handler/EvccLoadpointHandler.java | 6 +- .../evcc/internal/handler/EvccPvHandler.java | 6 +- .../internal/handler/EvccSiteHandler.java | 6 +- .../handler/EvccStatisticsHandler.java | 2 +- .../handler/EvccThingLifecycleAware.java | 2 +- .../internal/handler/EvccVehicleHandler.java | 6 +- .../handler/EvccBaseThingHandlerTest.java | 186 ++++++++++++++++++ 12 files changed, 212 insertions(+), 27 deletions(-) create mode 100644 bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java 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 9557457feabb9..5b84a29e5fee8 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 @@ -67,7 +67,7 @@ public EvccDiscoveryService() { @Override protected void startScan() { logger.debug("Starting evcc Discover"); - JsonObject state = thingHandler.getCachedEvccState(); + JsonObject state = thingHandler.getCachedEvccState().deepCopy(); if (!state.isEmpty()) { for (EvccDiscoveryMapper mapper : mappers) { mapper.discover(state, thingHandler).forEach(thing -> { diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java index 7540a8f3347fd..fd02446e48bf9 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java @@ -146,7 +146,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - private void createChannel(String thingKey, ThingBuilder builder, JsonElement value) { + protected void createChannel(String thingKey, ThingBuilder builder, JsonElement value) { ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, thingKey); ItemTypeUnit typeUnit = getItemType(channelTypeUID); String itemType = typeUnit.itemType; @@ -192,7 +192,7 @@ private ItemTypeUnit getItemType(ChannelTypeUID channelTypeUID) { return new ItemTypeUnit(channelType, Units.ONE); } - private void setItemValue(ItemTypeUnit itemTypeUnit, ChannelUID channelUID, JsonElement value) { + protected void setItemValue(ItemTypeUnit itemTypeUnit, ChannelUID channelUID, JsonElement value) { if (value.isJsonNull() || itemTypeUnit.itemType.isEmpty()) { return; } @@ -250,8 +250,7 @@ protected String getThingKey(String key) { return (type + "-" + Utils.sanitizeChannelID(key)); } - @Override - public void updateFromEvccState(JsonObject state) { + public void updateStatesFromApiResponse(JsonObject state) { if (!isInitialized || state.isEmpty()) { return; } @@ -369,7 +368,7 @@ protected void logUnknownChannelXml(String key, String itemType) { } } - private static class ItemTypeUnit { + protected static class ItemTypeUnit { private final Unit unit; private final String unitHint; private final String itemType; 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 6db67ac62f66c..c768cdddfceda 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 @@ -46,7 +46,7 @@ public EvccBatteryHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) public void initialize() { super.initialize(); Optional.ofNullable(bridgeHandler).ifPresent(handler -> { - JsonObject stateOpt = handler.getCachedEvccState(); + JsonObject stateOpt = handler.getCachedEvccState().deepCopy(); if (stateOpt.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); return; @@ -58,10 +58,10 @@ public void initialize() { } @Override - public void updateFromEvccState(JsonObject state) { + public void prepareApiResponseForChannelStateUpdate(JsonObject state) { state = state.has(JSON_MEMBER_BATTERY) ? state.getAsJsonArray(JSON_MEMBER_BATTERY).get(index).getAsJsonObject() : new JsonObject(); - super.updateFromEvccState(state); + super.updateStatesFromApiResponse(state); } @Override diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBridgeHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBridgeHandler.java index 5597b49572728..41a863acabdc3 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBridgeHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBridgeHandler.java @@ -150,7 +150,7 @@ public Optional fetchEvccState() { private void notifyListeners(JsonObject state) { for (EvccThingLifecycleAware listener : listeners) { try { - listener.updateFromEvccState(state); + listener.prepareApiResponseForChannelStateUpdate(state); } catch (Exception e) { if (listener instanceof BaseThingHandler handler) { logger.warn("Listener {} couldn't parse evcc state", handler.getThing().getUID(), e); @@ -167,7 +167,7 @@ public JsonObject getCachedEvccState() { public void register(EvccThingLifecycleAware handler) { listeners.addIfAbsent(handler); - Optional.of(lastState).ifPresent(handler::updateFromEvccState); + Optional.of(lastState).ifPresent(handler::prepareApiResponseForChannelStateUpdate); } public void unregister(EvccThingLifecycleAware handler) { 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 77af6b03de344..6c2d9945fb2e1 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 @@ -69,9 +69,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { } @Override - public void updateFromEvccState(JsonObject state) { + public void prepareApiResponseForChannelStateUpdate(JsonObject state) { updateJSON(state); - super.updateFromEvccState(state); + super.updateStatesFromApiResponse(state); } protected void updateJSON(JsonObject state) { diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java index fefe8801916ac..481d33fec4f78 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java @@ -61,7 +61,7 @@ public void initialize() { super.initialize(); Optional.ofNullable(bridgeHandler).ifPresent(handler -> { endpoint = handler.getBaseURL() + API_PATH_LOADPOINTS + "/" + (index + 1); - JsonObject stateOpt = handler.getCachedEvccState(); + JsonObject stateOpt = handler.getCachedEvccState().deepCopy(); if (stateOpt.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); return; @@ -110,11 +110,11 @@ public void handleCommand(ChannelUID channelUID, Command command) { } @Override - public void updateFromEvccState(JsonObject state) { + public void prepareApiResponseForChannelStateUpdate(JsonObject state) { version = Utils.convertVersionStringToIntArray(state.get("version").getAsString().split(" ")[0]); state = state.getAsJsonArray(JSON_MEMBER_LOADPOINTS).get(index).getAsJsonObject(); modifyJSON(state); - super.updateFromEvccState(state); + super.updateStatesFromApiResponse(state); } private void modifyJSON(JsonObject state) { diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java index ff551ec91c5ba..82d2be8573269 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java @@ -46,7 +46,7 @@ public EvccPvHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) { public void initialize() { super.initialize(); Optional.ofNullable(bridgeHandler).ifPresent(handler -> { - JsonObject stateOpt = handler.getCachedEvccState(); + JsonObject stateOpt = handler.getCachedEvccState().deepCopy(); if (stateOpt.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); return; @@ -58,9 +58,9 @@ public void initialize() { } @Override - public void updateFromEvccState(JsonObject state) { + public void prepareApiResponseForChannelStateUpdate(JsonObject state) { state = state.getAsJsonArray(JSON_MEMBER_PV).get(index).getAsJsonObject(); - super.updateFromEvccState(state); + super.updateStatesFromApiResponse(state); } @Override 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 7a03519ab523b..28c13b13f1bde 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 @@ -66,11 +66,11 @@ public void handleCommand(ChannelUID channelUID, Command command) { } @Override - public void updateFromEvccState(JsonObject state) { + public void prepareApiResponseForChannelStateUpdate(JsonObject state) { if (state.has("gridConfigured")) { modifyJSON(state); } - super.updateFromEvccState(state); + super.updateStatesFromApiResponse(state); } @Override @@ -78,7 +78,7 @@ public void initialize() { super.initialize(); Optional.ofNullable(bridgeHandler).ifPresent(handler -> { endpoint = handler.getBaseURL(); - JsonObject state = handler.getCachedEvccState(); + JsonObject state = handler.getCachedEvccState().deepCopy(); if (state.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); return; diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java index cd946993364dc..e6de0be2c8ae0 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java @@ -61,7 +61,7 @@ public JsonObject getStateFromCachedState(JsonObject state) { } @Override - public void updateFromEvccState(JsonObject state) { + public void prepareApiResponseForChannelStateUpdate(JsonObject state) { state = state.has(JSON_MEMBER_STATISTICS) ? state.getAsJsonObject(JSON_MEMBER_STATISTICS) : new JsonObject(); for (String statisticsKey : state.keySet()) { JsonObject statistic = state.getAsJsonObject(statisticsKey); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccThingLifecycleAware.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccThingLifecycleAware.java index d8c6af4a6479a..88376fd0ac924 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccThingLifecycleAware.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccThingLifecycleAware.java @@ -28,7 +28,7 @@ public interface EvccThingLifecycleAware { * * @param state the responded JSON */ - void updateFromEvccState(JsonObject state); + void prepareApiResponseForChannelStateUpdate(JsonObject state); /** * This method shall return the to the thing corresponding JSON object diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java index 20d3ee856f341..696e6f9397b4a 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java @@ -66,9 +66,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { } @Override - public void updateFromEvccState(JsonObject state) { + public void prepareApiResponseForChannelStateUpdate(JsonObject state) { state = state.getAsJsonObject(JSON_MEMBER_VEHICLES).getAsJsonObject(vehicleId); - super.updateFromEvccState(state); + super.updateStatesFromApiResponse(state); } @Override @@ -76,7 +76,7 @@ public void initialize() { super.initialize(); Optional.ofNullable(bridgeHandler).ifPresent(handler -> { endpoint = handler.getBaseURL() + API_PATH_VEHICLES; - JsonObject stateOpt = handler.getCachedEvccState(); + JsonObject stateOpt = handler.getCachedEvccState().deepCopy(); if (stateOpt.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); return; diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java new file mode 100644 index 0000000000000..78015f1160928 --- /dev/null +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.evcc.internal.handler; + +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 static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; +import org.junit.jupiter.api.*; +import org.openhab.core.thing.*; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.type.ChannelTypeRegistry; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +/** + * The {@link EvccBaseThingHandlerTest} is responsible for testing the BaseThingHandler implementation + * + * @author Marcel Goerentz - Initial contribution + */ +public class EvccBaseThingHandlerTest { + + private Thing thing; + private TestEvccBaseThingHandler handler; + + // Concrete subclass for testing + private static class TestEvccBaseThingHandler extends EvccBaseThingHandler { + public boolean setItemValueCalled = false; + public boolean createChannelCalled = false; + public boolean updateThingCalled = false; + public boolean updateStatusCalled = false; + public boolean prepareApiResponseForChannelStateUpdateCalled = true; + public ThingStatus lastUpdatedStatus = ThingStatus.UNKNOWN; + + public TestEvccBaseThingHandler(Thing thing, ChannelTypeRegistry registry) { + super(thing, registry); + } + + @Override + protected void updateThing(@NonNull Thing thing) { + updateThingCalled = true; + } + + @Override + protected void updateStatus(@NonNull ThingStatus status) { + lastUpdatedStatus = status; + updateStatusCalled = true; + } + + @Override + protected void createChannel(@NonNull String thingKey, @NonNull ThingBuilder builder, + @NonNull JsonElement value) { + createChannelCalled = true; + super.createChannel(thingKey, builder, value); + } + + @Override + protected void setItemValue(@NonNull ItemTypeUnit itemTypeUnit, @NonNull ChannelUID channelUID, + @NonNull JsonElement value) { + setItemValueCalled = true; + super.setItemValue(itemTypeUnit, channelUID, value); + } + + @Override + public void prepareApiResponseForChannelStateUpdate(@NonNull JsonObject state) { + prepareApiResponseForChannelStateUpdateCalled = true; + super.updateStatesFromApiResponse(state); + } + + @NonNull + @Override + public JsonObject getStateFromCachedState(@NonNull JsonObject state) { + return new JsonObject(); + } + } + + @BeforeEach + public void setUp() { + thing = mock(Thing.class); + ChannelTypeRegistry channelTypeRegistry = mock(ChannelTypeRegistry.class); + handler = spy(new TestEvccBaseThingHandler(thing, channelTypeRegistry)); + when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); + when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "battery")); + when(thing.getChannels()).thenReturn(Collections.emptyList()); + } + + @Test + public void testUpdateFromEvccStateNotInitializedDoesNothing() { + handler.isInitialized = false; + JsonObject state = new JsonObject(); + handler.updateStatesFromApiResponse(state); + assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); + assertFalse(handler.setItemValueCalled); + assertFalse(handler.createChannelCalled); + assertFalse(handler.updateThingCalled); + assertFalse(handler.updateStatusCalled); + assertEquals(ThingStatus.UNKNOWN, handler.lastUpdatedStatus); + } + + @Test + public void testUpdateFromEvccStateEmptyStateDoesNothing() { + handler.isInitialized = true; + JsonObject state = new JsonObject(); + handler.updateStatesFromApiResponse(state); + assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); + assertFalse(handler.setItemValueCalled); + assertFalse(handler.createChannelCalled); + assertFalse(handler.updateThingCalled); + // Status should not be updated for empty state + assertFalse(handler.updateStatusCalled); + assertEquals(ThingStatus.UNKNOWN, handler.lastUpdatedStatus); + } + + @Test + public void testUpdateFromEvccStateWithPrimitiveValueCreatesChannelAndSetsItemValue() { + handler.isInitialized = true; + JsonObject state = new JsonObject(); + state.add("capacity", new JsonPrimitive(5.5)); + // Channel does not exist + when(thing.getChannel(anyString())).thenReturn(null); + + handler.updateStatesFromApiResponse(state); + assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); + assertTrue(handler.createChannelCalled); + assertTrue(handler.setItemValueCalled); + assertTrue(handler.updateThingCalled); + assertTrue(handler.updateStatusCalled); + assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); + } + + @Test + public void testUpdateFromEvccStateWithExistingChannelDoesNotCreateChannel() { + handler.isInitialized = true; + JsonObject state = new JsonObject(); + state.add("capacity", new JsonPrimitive(5.5)); + Channel mockChannel = mock(Channel.class); + when(thing.getChannel(anyString())).thenReturn(mockChannel); + + handler.updateStatesFromApiResponse(state); + + assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); + assertFalse(handler.createChannelCalled); + assertTrue(handler.setItemValueCalled); + assertFalse(handler.updateThingCalled); // Should not update thing if channel exists + assertTrue(handler.updateStatusCalled); + assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); + } + + @Test + public void testUpdateFromEvccStateSkipsNonPrimitiveValues() { + handler.isInitialized = true; + JsonObject state = new JsonObject(); + JsonObject nonPrimitive = new JsonObject(); + nonPrimitive.addProperty("foo", "bar"); + state.add("complexKey", nonPrimitive); + + handler.updateStatesFromApiResponse(state); + + assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); + assertFalse(handler.createChannelCalled); + assertFalse(handler.setItemValueCalled); + assertFalse(handler.updateThingCalled); + assertTrue(handler.updateStatusCalled); // Status is updated even if nothing else happens + assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); + } +} From 39a5fab2d160acbf602fe04dd5e5797e89553cd2 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Tue, 26 Aug 2025 18:06:14 +0200 Subject: [PATCH 04/29] Update EvccBaseThingHandlerTest.java so no file gets created Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../binding/evcc/internal/handler/EvccBaseThingHandlerTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java index 78015f1160928..0760e852bb597 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java @@ -71,14 +71,12 @@ protected void updateStatus(@NonNull ThingStatus status) { protected void createChannel(@NonNull String thingKey, @NonNull ThingBuilder builder, @NonNull JsonElement value) { createChannelCalled = true; - super.createChannel(thingKey, builder, value); } @Override protected void setItemValue(@NonNull ItemTypeUnit itemTypeUnit, @NonNull ChannelUID channelUID, @NonNull JsonElement value) { setItemValueCalled = true; - super.setItemValue(itemTypeUnit, channelUID, value); } @Override From ead0de6e8e3bd103d7fce97d1dde38038447bd99 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:48:53 +0200 Subject: [PATCH 05/29] Fix SAT warnings Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../handler/EvccBaseThingHandlerTest.java | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java index 0760e852bb597..62a1a2631d97d 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java @@ -23,9 +23,14 @@ import java.util.Collections; import java.util.Map; -import org.eclipse.jdt.annotation.NonNull; -import org.junit.jupiter.api.*; -import org.openhab.core.thing.*; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.type.ChannelTypeRegistry; @@ -33,15 +38,22 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; +import io.micrometer.common.lang.Nullable; + /** * The {@link EvccBaseThingHandlerTest} is responsible for testing the BaseThingHandler implementation * * @author Marcel Goerentz - Initial contribution */ +@NonNullByDefault public class EvccBaseThingHandlerTest { - private Thing thing; - private TestEvccBaseThingHandler handler; + @Nullable + private Thing thing = mock(Thing.class); + @Nullable + private ChannelTypeRegistry channelTypeRegistry = mock(ChannelTypeRegistry.class); + @Nullable + private TestEvccBaseThingHandler handler = new TestEvccBaseThingHandler(thing, channelTypeRegistry); // Concrete subclass for testing private static class TestEvccBaseThingHandler extends EvccBaseThingHandler { @@ -57,37 +69,34 @@ public TestEvccBaseThingHandler(Thing thing, ChannelTypeRegistry registry) { } @Override - protected void updateThing(@NonNull Thing thing) { + protected void updateThing(Thing thing) { updateThingCalled = true; } @Override - protected void updateStatus(@NonNull ThingStatus status) { + protected void updateStatus(ThingStatus status) { lastUpdatedStatus = status; updateStatusCalled = true; } @Override - protected void createChannel(@NonNull String thingKey, @NonNull ThingBuilder builder, - @NonNull JsonElement value) { + protected void createChannel(String thingKey, ThingBuilder builder, JsonElement value) { createChannelCalled = true; } @Override - protected void setItemValue(@NonNull ItemTypeUnit itemTypeUnit, @NonNull ChannelUID channelUID, - @NonNull JsonElement value) { + protected void setItemValue(ItemTypeUnit itemTypeUnit, ChannelUID channelUID, JsonElement value) { setItemValueCalled = true; } @Override - public void prepareApiResponseForChannelStateUpdate(@NonNull JsonObject state) { + public void prepareApiResponseForChannelStateUpdate(JsonObject state) { prepareApiResponseForChannelStateUpdateCalled = true; super.updateStatesFromApiResponse(state); } - @NonNull @Override - public JsonObject getStateFromCachedState(@NonNull JsonObject state) { + public JsonObject getStateFromCachedState(JsonObject state) { return new JsonObject(); } } From bfe2c832a6ef7616f1e6f2fc92f6874024c3a1cc Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:22:59 +0200 Subject: [PATCH 06/29] Fix copilot findings Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../src/main/resources/OH-INF/thing/loadpoint.xml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml index ccc699de232c1..3b132fb4e2560 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml @@ -430,34 +430,37 @@ - Number:ElectricCurrent + Number:ElectricCurrent Actual charge current of charger phase L1 + Energy Status Info - + - Number:ElectricCurrent + Number:ElectricCurrent Actual charge current of charger phase L2 + Energy Status Info - + - Number:ElectricCurrent + Number:ElectricCurrent Actual charge current of charger phase L3 + Energy Status Info - + Number From cad2f94e4065072db78e5a6fe92f21e9848e2be5 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:57:59 +0200 Subject: [PATCH 07/29] Add more unit tests Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../handler/EvccBaseThingHandlerTest.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java index 62a1a2631d97d..2ad185044537a 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java @@ -15,10 +15,15 @@ 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 static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_ENERGY; import java.util.Collections; import java.util.Map; @@ -32,7 +37,9 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.type.ChannelType; import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.openhab.core.types.RefreshType; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -190,4 +197,73 @@ public void testUpdateFromEvccStateSkipsNonPrimitiveValues() { assertTrue(handler.updateStatusCalled); // Status is updated even if nothing else happens assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); } + + @Test + public void testHandleCommandWithNumberItemType() { + ChannelUID channelUID = new ChannelUID("test:thing:uid:battery-capacity"); + RefreshType command = RefreshType.REFRESH; + + JsonObject cachedState = new JsonObject(); + cachedState.add("capacity", new JsonPrimitive(42.0)); + + ChannelType mockChannelType = mock(ChannelType.class); + when(mockChannelType.getItemType()).thenReturn(NUMBER_ENERGY); + when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType); + + doReturn(cachedState).when(handler).getStateFromCachedState(any()); + handler.bridgeHandler = mock(EvccBridgeHandler.class); + + handler.handleCommand(channelUID, command); + + assertTrue(handler.setItemValueCalled); + } + + @Test + public void testHandleCommandWithRefreshTypeAndValidValue() { + ChannelUID channelUID = new ChannelUID("test:thing:uid:battery-capacity"); + RefreshType command = RefreshType.REFRESH; + + JsonObject cachedState = new JsonObject(); + cachedState.add("capacity", new JsonPrimitive(42.0)); + + ChannelType mockChannelType = mock(ChannelType.class); + when(mockChannelType.getItemType()).thenReturn(NUMBER_ENERGY); + when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType); + + doReturn(cachedState).when(handler).getStateFromCachedState(any()); + handler.bridgeHandler = mock(EvccBridgeHandler.class); + + handler.handleCommand(channelUID, command); + + assertTrue(handler.setItemValueCalled); + } + + @Test + public void testHandleCommandWithRefreshTypeAndMissingValue() { + ChannelUID channelUID = new ChannelUID("test:thing:uid:battery-capacity"); + RefreshType command = RefreshType.REFRESH; + + JsonObject cachedState = new JsonObject(); // no "capacity" key + doReturn(cachedState).when(handler).getStateFromCachedState(any()); + + handler.handleCommand(channelUID, command); + + assertFalse(handler.setItemValueCalled); + } + + @Test + public void testCreateChannelWithUnknownItemType() { + JsonElement value = new JsonPrimitive(5.5); + ThingBuilder builder = mock(ThingBuilder.class); + + // Mock ChannelType mit "Unknown" als ItemType + ChannelType mockChannelType = mock(ChannelType.class); + when(mockChannelType.getItemType()).thenReturn(NUMBER_ENERGY); + when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType); + + handler.createChannel("capacity", builder, value); + + assertTrue(handler.createChannelCalled); // Flag gesetzt + verify(builder, never()).withChannel(any()); // Kein Channel sollte hinzugefügt werden + } } From 36140fe67672386fefb160883938e5f7e29dfa37 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Thu, 28 Aug 2025 14:57:20 +0200 Subject: [PATCH 08/29] Add unit tests for EvccBaseThingHandler, structure the unit tests, add channels for voltage measurements Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../handler/EvccLoadpointHandler.java | 17 +- .../internal/handler/EvccSiteHandler.java | 19 +- .../resources/OH-INF/i18n/evcc.properties | 12 + .../main/resources/OH-INF/thing/loadpoint.xml | 45 ++- .../src/main/resources/OH-INF/thing/site.xml | 33 ++ .../handler/EvccBaseThingHandlerTest.java | 362 ++++++++++++------ 6 files changed, 355 insertions(+), 133 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java index 481d33fec4f78..2d9f99ae2a581 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java @@ -29,6 +29,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -121,11 +122,9 @@ private void modifyJSON(JsonObject state) { JSON_KEYS.forEach((oldKey, newKey) -> { if (state.has(oldKey)) { if (oldKey.equals("chargeCurrents")) { - int phase = 1; - for (JsonElement current : state.getAsJsonArray(oldKey)) { - state.add("chargeCurrentL" + phase, current); - phase++; - } + addMeasurementDatapointsToState(state, state.getAsJsonArray(oldKey), "Current"); + } else if (oldKey.equals("chargeVoltages")) { + addMeasurementDatapointsToState(state, state.getAsJsonArray(oldKey), "Voltage"); } else { state.add(newKey, state.get(oldKey)); } @@ -134,6 +133,14 @@ private void modifyJSON(JsonObject state) { }); } + protected void addMeasurementDatapointsToState(JsonObject state, JsonArray values, String datapoint) { + int phase = 1; + for (JsonElement value : values) { + state.add("charge" + datapoint + "L" + phase, value); + phase++; + } + } + @Override public JsonObject getStateFromCachedState(JsonObject state) { return state.has(JSON_MEMBER_LOADPOINTS) 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 28c13b13f1bde..47981fd3acf3e 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 @@ -27,6 +27,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -98,12 +99,10 @@ public void initialize() { private void modifyJSON(JsonObject state) { for (Map.Entry entry : state.getAsJsonObject("grid").entrySet()) { - if (entry.getKey().equals("currents")) { - int phase = 1; - for (JsonElement value : entry.getValue().getAsJsonArray()) { - state.add("gridCurrentL" + phase, value); - phase++; - } + if ("currents".equals(entry.getKey())) { + addMeasurementDatapointsToState(state, entry.getValue().getAsJsonArray(), "Current"); + } else if ("voltages".equals(entry.getKey())) { + addMeasurementDatapointsToState(state, entry.getValue().getAsJsonArray(), "Voltage"); } else { state.add("grid" + Utils.capitalizeFirstLetter(entry.getKey()), entry.getValue()); } @@ -112,6 +111,14 @@ private void modifyJSON(JsonObject state) { state.remove("gridConfigured"); } + protected void addMeasurementDatapointsToState(JsonObject state, JsonArray values, String datapoint) { + int phase = 1; + for (JsonElement value : values) { + state.add("grid" + datapoint + "L" + phase, value); + phase++; + } + } + @Override public JsonObject getStateFromCachedState(JsonObject state) { return state; diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties index 8845459e7d15b..68ca34c1f6fb8 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties @@ -84,6 +84,12 @@ channel-type.evcc.loadpoint-charge-remaining-energy.label = Charging Remaining E channel-type.evcc.loadpoint-charge-remaining-energy.description = Remaining energy until limit SoC is reached channel-type.evcc.loadpoint-charge-total-import.label = Charge Total Import channel-type.evcc.loadpoint-charge-total-import.description = Total imported energy +channel-type.evcc.loadpoint-charge-voltage-l1.label = Charge Voltage 1 +channel-type.evcc.loadpoint-charge-voltage-l1.description = Actual charge voltage of charger phase L1 +channel-type.evcc.loadpoint-charge-voltage-l2.label = Charge Voltage 2 +channel-type.evcc.loadpoint-charge-voltage-l2.description = Actual charge voltage of charger phase L2 +channel-type.evcc.loadpoint-charge-voltage-l3.label = Charge Voltage 3 +channel-type.evcc.loadpoint-charge-voltage-l3.description = Actual charge voltage of charger phase L3 channel-type.evcc.loadpoint-charged-energy.label = Charged Energy channel-type.evcc.loadpoint-charged-energy.description = Energy charged since plugged-in channel-type.evcc.loadpoint-charger-feature-heating.label = Heating @@ -320,6 +326,12 @@ channel-type.evcc.site-grid-energy.label = Grid Energy channel-type.evcc.site-grid-energy.description = Consumed energy from the grid channel-type.evcc.site-grid-power.label = Grid Power channel-type.evcc.site-grid-power.description = Current power from grid (negative means feed-in) +channel-type.evcc.site-grid-voltage-l1.label = Charge Voltage 1 +channel-type.evcc.site-grid-voltage-l1.description = Actual charge voltage of charger phase L1 +channel-type.evcc.site-grid-voltage-l2.label = Charge Voltage 2 +channel-type.evcc.site-grid-voltage-l2.description = Actual charge voltage of charger phase L2 +channel-type.evcc.site-grid-voltage-l3.label = Charge Voltage 3 +channel-type.evcc.site-grid-voltage-l3.description = Actual charge voltage of charger phase L3 channel-type.evcc.site-home-power.label = Home Power channel-type.evcc.site-home-power.description = Current power consumption by home channel-type.evcc.site-interval.label = Interval diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml index 3b132fb4e2560..0777f30cb717f 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml @@ -435,8 +435,8 @@ Actual charge current of charger phase L1 Energy - Status - Info + Measurement + Current @@ -446,8 +446,8 @@ Actual charge current of charger phase L2 Energy - Status - Info + Measurement + Current @@ -457,8 +457,41 @@ Actual charge current of charger phase L3 Energy - Status - Info + Measurement + Current + + + + + Number:ElectricPotential + + Actual charge voltage of charger phase L1 + Energy + + Measurement + Voltage + + + + + Number:ElectricPotential + + Actual charge voltage of charger phase L2 + Energy + + Measurement + Voltage + + + + + Number:ElectricPotential + + Actual charge voltage of charger phase L3 + Energy + + Measurement + Voltage diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml index 4be27b8a38564..9d2b5dfc78d8f 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml @@ -228,6 +228,39 @@ + + Number:ElectricPotential + + Actual charge voltage of charger phase L1 + Energy + + Measurement + Voltage + + + + + Number:ElectricPotential + + Actual charge voltage of charger phase L2 + Energy + + Measurement + Voltage + + + + + Number:ElectricPotential + + Actual charge voltage of charger phase L3 + Energy + + Measurement + Voltage + + + Number:Energy diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java index 2ad185044537a..48e8ba679f9a1 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java @@ -23,14 +23,35 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_CURRENCY; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_DIMENSIONLESS; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_ELECTRIC_CURRENT; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_EMISSION_INTENSITY; import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_ENERGY; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_ENERGY_PRICE; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_LENGTH; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_POWER; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_TEMPERATURE; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_TIME; import java.util.Collections; import java.util.Map; +import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.openhab.binding.evcc.internal.handler.EvccBaseThingHandler.ItemTypeUnit; +import org.openhab.core.library.CoreItemFactory; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -40,6 +61,8 @@ import org.openhab.core.thing.type.ChannelType; import org.openhab.core.thing.type.ChannelTypeRegistry; import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -55,10 +78,12 @@ @NonNullByDefault public class EvccBaseThingHandlerTest { - @Nullable + @SuppressWarnings("null") private Thing thing = mock(Thing.class); - @Nullable + + @SuppressWarnings("null") private ChannelTypeRegistry channelTypeRegistry = mock(ChannelTypeRegistry.class); + @Nullable private TestEvccBaseThingHandler handler = new TestEvccBaseThingHandler(thing, channelTypeRegistry); @@ -69,7 +94,11 @@ private static class TestEvccBaseThingHandler extends EvccBaseThingHandler { public boolean updateThingCalled = false; public boolean updateStatusCalled = false; public boolean prepareApiResponseForChannelStateUpdateCalled = true; + public boolean logUnknownChannelXmlCalled = false; public ThingStatus lastUpdatedStatus = ThingStatus.UNKNOWN; + public boolean updateStateCalled = false; + public State lastState = UnDefType.UNDEF; + public ChannelUID lastChannelUID = new ChannelUID("dummy:dummy:dummy:dummy"); public TestEvccBaseThingHandler(Thing thing, ChannelTypeRegistry registry) { super(thing, registry); @@ -94,6 +123,7 @@ protected void createChannel(String thingKey, ThingBuilder builder, JsonElement @Override protected void setItemValue(ItemTypeUnit itemTypeUnit, ChannelUID channelUID, JsonElement value) { setItemValueCalled = true; + super.setItemValue(itemTypeUnit, channelUID, value); } @Override @@ -106,8 +136,22 @@ public void prepareApiResponseForChannelStateUpdate(JsonObject state) { public JsonObject getStateFromCachedState(JsonObject state) { return new JsonObject(); } + + @Override + public void updateState(ChannelUID uid, State state) { + updateStateCalled = true; + lastState = state; + lastChannelUID = uid; + } + + // Make sure no files are getting created + @Override + protected void logUnknownChannelXml(String key, String itemType) { + logUnknownChannelXmlCalled = true; + } } + @SuppressWarnings("null") @BeforeEach public void setUp() { thing = mock(Thing.class); @@ -118,152 +162,238 @@ public void setUp() { when(thing.getChannels()).thenReturn(Collections.emptyList()); } - @Test - public void testUpdateFromEvccStateNotInitializedDoesNothing() { - handler.isInitialized = false; - JsonObject state = new JsonObject(); - handler.updateStatesFromApiResponse(state); - assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); - assertFalse(handler.setItemValueCalled); - assertFalse(handler.createChannelCalled); - assertFalse(handler.updateThingCalled); - assertFalse(handler.updateStatusCalled); - assertEquals(ThingStatus.UNKNOWN, handler.lastUpdatedStatus); - } + @Nested + class UpdateStatesFromApiResponseTests { + @Test + public void updateFromEvccStateNotInitializedDoesNothing() { + handler.isInitialized = false; + JsonObject state = new JsonObject(); + handler.updateStatesFromApiResponse(state); + assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); + assertFalse(handler.setItemValueCalled); + assertFalse(handler.createChannelCalled); + assertFalse(handler.updateThingCalled); + assertFalse(handler.updateStatusCalled); + assertEquals(ThingStatus.UNKNOWN, handler.lastUpdatedStatus); + } - @Test - public void testUpdateFromEvccStateEmptyStateDoesNothing() { - handler.isInitialized = true; - JsonObject state = new JsonObject(); - handler.updateStatesFromApiResponse(state); - assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); - assertFalse(handler.setItemValueCalled); - assertFalse(handler.createChannelCalled); - assertFalse(handler.updateThingCalled); - // Status should not be updated for empty state - assertFalse(handler.updateStatusCalled); - assertEquals(ThingStatus.UNKNOWN, handler.lastUpdatedStatus); - } + @Test + public void updateFromEvccStateEmptyStateDoesNothing() { + handler.isInitialized = true; + JsonObject state = new JsonObject(); + handler.updateStatesFromApiResponse(state); + assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); + assertFalse(handler.setItemValueCalled); + assertFalse(handler.createChannelCalled); + assertFalse(handler.updateThingCalled); + // Status should not be updated for empty state + assertFalse(handler.updateStatusCalled); + assertEquals(ThingStatus.UNKNOWN, handler.lastUpdatedStatus); + } - @Test - public void testUpdateFromEvccStateWithPrimitiveValueCreatesChannelAndSetsItemValue() { - handler.isInitialized = true; - JsonObject state = new JsonObject(); - state.add("capacity", new JsonPrimitive(5.5)); - // Channel does not exist - when(thing.getChannel(anyString())).thenReturn(null); - - handler.updateStatesFromApiResponse(state); - assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); - assertTrue(handler.createChannelCalled); - assertTrue(handler.setItemValueCalled); - assertTrue(handler.updateThingCalled); - assertTrue(handler.updateStatusCalled); - assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); - } + @Test + public void updateFromEvccStateWithPrimitiveValueCreatesChannelAndSetsItemValue() { + handler.isInitialized = true; + JsonObject state = new JsonObject(); + state.add("capacity", new JsonPrimitive(5.5)); + // Channel does not exist + when(thing.getChannel(anyString())).thenReturn(null); + + handler.updateStatesFromApiResponse(state); + assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); + assertTrue(handler.createChannelCalled); + assertTrue(handler.setItemValueCalled); + assertTrue(handler.updateThingCalled); + assertTrue(handler.updateStatusCalled); + assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); + } - @Test - public void testUpdateFromEvccStateWithExistingChannelDoesNotCreateChannel() { - handler.isInitialized = true; - JsonObject state = new JsonObject(); - state.add("capacity", new JsonPrimitive(5.5)); - Channel mockChannel = mock(Channel.class); - when(thing.getChannel(anyString())).thenReturn(mockChannel); - - handler.updateStatesFromApiResponse(state); - - assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); - assertFalse(handler.createChannelCalled); - assertTrue(handler.setItemValueCalled); - assertFalse(handler.updateThingCalled); // Should not update thing if channel exists - assertTrue(handler.updateStatusCalled); - assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); - } + @Test + public void updateFromEvccStateWithExistingChannelDoesNotCreateChannel() { + handler.isInitialized = true; + JsonObject state = new JsonObject(); + state.add("capacity", new JsonPrimitive(5.5)); + @SuppressWarnings("null") + Channel mockChannel = mock(Channel.class); + when(thing.getChannel(anyString())).thenReturn(mockChannel); + + handler.updateStatesFromApiResponse(state); + + assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); + assertFalse(handler.createChannelCalled); + assertTrue(handler.setItemValueCalled); + assertFalse(handler.updateThingCalled); // Should not update thing if channel exists + assertTrue(handler.updateStatusCalled); + assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); + } - @Test - public void testUpdateFromEvccStateSkipsNonPrimitiveValues() { - handler.isInitialized = true; - JsonObject state = new JsonObject(); - JsonObject nonPrimitive = new JsonObject(); - nonPrimitive.addProperty("foo", "bar"); - state.add("complexKey", nonPrimitive); - - handler.updateStatesFromApiResponse(state); - - assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); - assertFalse(handler.createChannelCalled); - assertFalse(handler.setItemValueCalled); - assertFalse(handler.updateThingCalled); - assertTrue(handler.updateStatusCalled); // Status is updated even if nothing else happens - assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); + @Test + public void updateFromEvccStateSkipsNonPrimitiveValues() { + handler.isInitialized = true; + JsonObject state = new JsonObject(); + JsonObject nonPrimitive = new JsonObject(); + nonPrimitive.addProperty("foo", "bar"); + state.add("complexKey", nonPrimitive); + + handler.updateStatesFromApiResponse(state); + + assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); + assertFalse(handler.createChannelCalled); + assertFalse(handler.setItemValueCalled); + assertFalse(handler.updateThingCalled); + assertTrue(handler.updateStatusCalled); // Status is updated even if nothing else happens + assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); + } } - @Test - public void testHandleCommandWithNumberItemType() { - ChannelUID channelUID = new ChannelUID("test:thing:uid:battery-capacity"); - RefreshType command = RefreshType.REFRESH; + @Nested + class HandleCommandTests { + @SuppressWarnings("null") + @Test + public void handleCommandWithNumberItemType() { + ChannelUID channelUID = new ChannelUID("test:thing:uid:battery-capacity"); + RefreshType command = RefreshType.REFRESH; - JsonObject cachedState = new JsonObject(); - cachedState.add("capacity", new JsonPrimitive(42.0)); + JsonObject cachedState = new JsonObject(); + cachedState.add("capacity", new JsonPrimitive(42.0)); - ChannelType mockChannelType = mock(ChannelType.class); - when(mockChannelType.getItemType()).thenReturn(NUMBER_ENERGY); - when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType); + ChannelType mockChannelType = mock(ChannelType.class); + when(mockChannelType.getItemType()).thenReturn(NUMBER_ENERGY); + when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType); - doReturn(cachedState).when(handler).getStateFromCachedState(any()); - handler.bridgeHandler = mock(EvccBridgeHandler.class); + doReturn(cachedState).when(handler).getStateFromCachedState(any()); + handler.bridgeHandler = mock(EvccBridgeHandler.class); - handler.handleCommand(channelUID, command); + handler.handleCommand(channelUID, command); - assertTrue(handler.setItemValueCalled); - } + assertTrue(handler.setItemValueCalled); + } - @Test - public void testHandleCommandWithRefreshTypeAndValidValue() { - ChannelUID channelUID = new ChannelUID("test:thing:uid:battery-capacity"); - RefreshType command = RefreshType.REFRESH; + @SuppressWarnings("null") + @Test + public void handleCommandWithRefreshTypeAndValidValue() { + ChannelUID channelUID = new ChannelUID("test:thing:uid:battery-capacity"); + RefreshType command = RefreshType.REFRESH; - JsonObject cachedState = new JsonObject(); - cachedState.add("capacity", new JsonPrimitive(42.0)); + JsonObject cachedState = new JsonObject(); + cachedState.add("capacity", new JsonPrimitive(42.0)); - ChannelType mockChannelType = mock(ChannelType.class); - when(mockChannelType.getItemType()).thenReturn(NUMBER_ENERGY); - when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType); + ChannelType mockChannelType = mock(ChannelType.class); + when(mockChannelType.getItemType()).thenReturn(NUMBER_ENERGY); + when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType); - doReturn(cachedState).when(handler).getStateFromCachedState(any()); - handler.bridgeHandler = mock(EvccBridgeHandler.class); + doReturn(cachedState).when(handler).getStateFromCachedState(any()); + handler.bridgeHandler = mock(EvccBridgeHandler.class); - handler.handleCommand(channelUID, command); + handler.handleCommand(channelUID, command); - assertTrue(handler.setItemValueCalled); - } + assertTrue(handler.setItemValueCalled); + } - @Test - public void testHandleCommandWithRefreshTypeAndMissingValue() { - ChannelUID channelUID = new ChannelUID("test:thing:uid:battery-capacity"); - RefreshType command = RefreshType.REFRESH; + @SuppressWarnings("null") + @Test + public void handleCommandWithRefreshTypeAndMissingValue() { + ChannelUID channelUID = new ChannelUID("test:thing:uid:battery-capacity"); + RefreshType command = RefreshType.REFRESH; - JsonObject cachedState = new JsonObject(); // no "capacity" key - doReturn(cachedState).when(handler).getStateFromCachedState(any()); + JsonObject cachedState = new JsonObject(); // no "capacity" key + doReturn(cachedState).when(handler).getStateFromCachedState(any()); - handler.handleCommand(channelUID, command); + handler.handleCommand(channelUID, command); - assertFalse(handler.setItemValueCalled); + assertFalse(handler.setItemValueCalled); + assertFalse(handler.logUnknownChannelXmlCalled); + } } @Test public void testCreateChannelWithUnknownItemType() { JsonElement value = new JsonPrimitive(5.5); + @SuppressWarnings("null") ThingBuilder builder = mock(ThingBuilder.class); - // Mock ChannelType mit "Unknown" als ItemType + @SuppressWarnings("null") ChannelType mockChannelType = mock(ChannelType.class); - when(mockChannelType.getItemType()).thenReturn(NUMBER_ENERGY); + when(mockChannelType.getItemType()).thenReturn("Unknown"); when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType); handler.createChannel("capacity", builder, value); - assertTrue(handler.createChannelCalled); // Flag gesetzt - verify(builder, never()).withChannel(any()); // Kein Channel sollte hinzugefügt werden + assertTrue(handler.createChannelCalled); + verify(builder, never()).withChannel(any()); + } + + @Nested + class GetThingKeyTests { + @Test + public void getThingKeyWithBatteryTypeAndSpecialKey() { + when(thing.getProperties()).thenReturn(Map.of("type", "battery")); + + String key = "soc"; + String result = handler.getThingKey(key); + + assertEquals("battery-soc", result); + } + + @Test + public void getThingKeyWithHeatingType() { + when(thing.getProperties()).thenReturn(Map.of("type", "heating")); + + String key = "capacity"; + String result = handler.getThingKey(key); + + assertEquals("loadpoint-capacity", result); + } + + @Test + public void getThingKeyWithDefaultType() { + when(thing.getProperties()).thenReturn(Map.of("type", "loadpoint")); + + String key = "someKey"; + String result = handler.getThingKey(key); + + assertEquals("loadpoint-some-key", result); + } + } + + @Nested + class SetItemValueTests { + + static Stream provideItemTypesWithExpectedStateClass() { + return Stream.of(Arguments.of(NUMBER_DIMENSIONLESS, QuantityType.class), + Arguments.of(NUMBER_ELECTRIC_CURRENT, QuantityType.class), + Arguments.of(NUMBER_EMISSION_INTENSITY, QuantityType.class), + Arguments.of(NUMBER_ENERGY, QuantityType.class), Arguments.of(NUMBER_LENGTH, QuantityType.class), + Arguments.of(NUMBER_POWER, QuantityType.class), Arguments.of(NUMBER_TIME, QuantityType.class), + Arguments.of(NUMBER_TEMPERATURE, QuantityType.class), + Arguments.of(CoreItemFactory.NUMBER, DecimalType.class), + Arguments.of(NUMBER_CURRENCY, DecimalType.class), + Arguments.of(NUMBER_ENERGY_PRICE, DecimalType.class), + Arguments.of(CoreItemFactory.STRING, StringType.class), + Arguments.of(CoreItemFactory.SWITCH, OnOffType.class)); + } + + @ParameterizedTest + @MethodSource("provideItemTypesWithExpectedStateClass") + void setItemValueWithVariousTypes(String itemType, Class expectedStateClass) { + ChannelUID channelUID = new ChannelUID("test:thing:uid:dummy"); + JsonElement value = itemType.equals(CoreItemFactory.STRING) ? new JsonPrimitive("OK") + : itemType.equals(CoreItemFactory.SWITCH) ? new JsonPrimitive(true) : new JsonPrimitive(12.5); + + ChannelType mockChannelType = mock(ChannelType.class); + when(mockChannelType.getItemType()).thenReturn(itemType); + if (NUMBER_TEMPERATURE.equals(itemType)) { + when(mockChannelType.getUnitHint()).thenReturn("°C"); + } + when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType); + + ItemTypeUnit itemTypeUnit = new ItemTypeUnit(mockChannelType, Units.ONE); + + handler.setItemValue(itemTypeUnit, channelUID, value); + + assertTrue(handler.updateStateCalled); + assertEquals(channelUID, handler.lastChannelUID); + assertEquals(expectedStateClass, handler.lastState.getClass()); + } } } From cabaa5483535a6bf3a8817b2454077717b936d9f Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Sun, 31 Aug 2025 22:17:58 +0200 Subject: [PATCH 09/29] Fix review findings Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../evcc/internal/EvccBindingConstants.java | 23 +++-- .../mapper/BatteryDiscoveryMapper.java | 7 +- .../mapper/LoadpointDiscoveryMapper.java | 8 +- .../discovery/mapper/PvDiscoveryMapper.java | 5 +- .../mapper/StatisticsDiscoveryMapper.java | 4 +- .../mapper/VehicleDiscoveryMapper.java | 9 +- .../handler/EvccBaseThingHandler.java | 34 +++++-- .../internal/handler/EvccBatteryHandler.java | 6 +- .../internal/handler/EvccHeatingHandler.java | 6 +- .../handler/EvccLoadpointHandler.java | 23 ++--- .../evcc/internal/handler/EvccPvHandler.java | 6 +- .../handler/EvccStatisticsHandler.java | 2 +- .../internal/handler/EvccVehicleHandler.java | 6 +- .../handler/BaseThingHandlerTestClass.java | 98 +++++++++++++++++++ .../handler/EvccBaseThingHandlerTest.java | 87 +++------------- 15 files changed, 192 insertions(+), 132 deletions(-) create mode 100644 bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/BaseThingHandlerTestClass.java 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 71e073af29af4..4f4c44cec7887 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 @@ -59,12 +59,23 @@ public class EvccBindingConstants { public static final String PROPERTY_TYPE_STATISTICS = "statistics"; public static final String PROPERTY_TYPE_VEHICLE = "vehicle"; - public static final String JSON_MEMBER_BATTERY = "battery"; - public static final String JSON_MEMBER_LOADPOINTS = "loadpoints"; - public static final String JSON_MEMBER_PV = "pv"; - public static final String JSON_MEMBER_STATISTICS = "statistics"; - public static final String JSON_MEMBER_VEHICLES = "vehicles"; - public static final String JSON_MEMBER_CHARGER_FEATURE_HEATING = "chargerFeatureHeating"; + public static final String JSON_KEY_BATTERY = "battery"; + public static final String JSON_KEY_LOADPOINTS = "loadpoints"; + public static final String JSON_KEY_PV = "pv"; + public static final String JSON_KEY_STATISTICS = "statistics"; + public static final String JSON_KEY_VEHICLES = "vehicles"; + public static final String JSON_KEY_CHARGER_FEATURE_HEATING = "chargerFeatureHeating"; + public static final String JSON_KEY_TITLE = "title"; + public static final String JSON_KEY_CHARGE_CURRENT = "chargeCurrent"; + public static final String JSON_KEY_OFFERED_CURRENT = "offeredCurrent"; + public static final String JSON_KEY_VEHICLE_PRESENT = "vehiclePresent"; + public static final String JSON_KEY_CONNECTED = "connected"; + public static final String JSON_KEY_ENABLED = "enabled"; + public static final String JSON_KEY_CHARGING = "charging"; + public static final String JSON_KEY_PHASES = "phases"; + public static final String JSON_KEY_PHASES_CONFIGURED = "phasesConfigured"; + public static final String JSON_KEY_CHARGE_CURRENTS = "chargeCurrents"; + public static final String JSON_KEY_CHARGE_VOLTAGES = "chargeVoltages"; public static final String NUMBER_CURRENCY = CoreItemFactory.NUMBER + ":Currency"; public static final String NUMBER_DIMENSIONLESS = CoreItemFactory.NUMBER + ":Dimensionless"; 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 170811960f0c6..01e5e7964c738 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 @@ -41,14 +41,15 @@ public class BatteryDiscoveryMapper implements EvccDiscoveryMapper { @Override public Collection discover(JsonObject state, EvccBridgeHandler bridgeHandler) { List results = new ArrayList<>(); - JsonArray batteries = state.getAsJsonArray(JSON_MEMBER_BATTERY); + 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("title") ? battery.get("title").getAsString().toLowerCase(Locale.ROOT) - : "battery" + i; + String title = battery.has(JSON_KEY_TITLE) + ? battery.get(JSON_KEY_TITLE).getAsString().toLowerCase(Locale.ROOT) + : JSON_KEY_BATTERY + i; ThingUID uid = new ThingUID(EvccBindingConstants.THING_TYPE_BATTERY, bridgeHandler.getThing().getUID(), Utils.sanitizeName(title)); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/LoadpointDiscoveryMapper.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/LoadpointDiscoveryMapper.java index cb28619302758..f113528e4cf0f 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/LoadpointDiscoveryMapper.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/LoadpointDiscoveryMapper.java @@ -43,21 +43,21 @@ public class LoadpointDiscoveryMapper implements EvccDiscoveryMapper { @Override public Collection discover(JsonObject state, EvccBridgeHandler bridgeHandler) { List results = new ArrayList<>(); - JsonArray loadpoints = state.getAsJsonArray(JSON_MEMBER_LOADPOINTS); + JsonArray loadpoints = state.getAsJsonArray(JSON_KEY_LOADPOINTS); if (loadpoints == null) { return results; } for (int i = 0; i < loadpoints.size(); i++) { JsonObject lp = loadpoints.get(i).getAsJsonObject(); - String title = lp.has("title") ? lp.get("title").getAsString().toLowerCase(Locale.ROOT) : "loadpoint" + i; + String title = lp.has(JSON_KEY_TITLE) ? lp.get(JSON_KEY_TITLE).getAsString().toLowerCase(Locale.ROOT) + : "loadpoint" + i; ThingUID uid = new ThingUID("DUMMY:DUMMY:DUMMY"); Map properties = new HashMap<>(); properties.put(PROPERTY_INDEX, i); properties.put(PROPERTY_TITLE, title); - if (lp.has(JSON_MEMBER_CHARGER_FEATURE_HEATING) - && lp.get(JSON_MEMBER_CHARGER_FEATURE_HEATING).getAsBoolean()) { + if (lp.has(JSON_KEY_CHARGER_FEATURE_HEATING) && lp.get(JSON_KEY_CHARGER_FEATURE_HEATING).getAsBoolean()) { uid = new ThingUID(EvccBindingConstants.THING_TYPE_HEATING, bridgeHandler.getThing().getUID(), Utils.sanitizeName(title)); properties.put(PROPERTY_TYPE, PROPERTY_TYPE_HEATING); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/PvDiscoveryMapper.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/PvDiscoveryMapper.java index d5db3bae1c41b..9c9ef725272fb 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/PvDiscoveryMapper.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/PvDiscoveryMapper.java @@ -41,13 +41,14 @@ public class PvDiscoveryMapper implements EvccDiscoveryMapper { @Override public Collection discover(JsonObject state, EvccBridgeHandler bridgeHandler) { List results = new ArrayList<>(); - JsonArray pvs = state.getAsJsonArray(JSON_MEMBER_PV); + JsonArray pvs = state.getAsJsonArray(JSON_KEY_PV); if (pvs == null) { return results; } for (int i = 0; i < pvs.size(); i++) { JsonObject pv = pvs.get(i).getAsJsonObject(); - String title = pv.has("title") ? pv.get("title").getAsString().toLowerCase(Locale.ROOT) : "pv" + i; + String title = pv.has(JSON_KEY_TITLE) ? pv.get(JSON_KEY_TITLE).getAsString().toLowerCase(Locale.ROOT) + : JSON_KEY_PV + i; ThingUID uid = new ThingUID(EvccBindingConstants.THING_TYPE_PV, bridgeHandler.getThing().getUID(), Utils.sanitizeName(title)); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/StatisticsDiscoveryMapper.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/StatisticsDiscoveryMapper.java index ef7a97b0ef2f4..8fd85a3423722 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/StatisticsDiscoveryMapper.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/StatisticsDiscoveryMapper.java @@ -38,12 +38,12 @@ public class StatisticsDiscoveryMapper implements EvccDiscoveryMapper { @Override public Collection discover(JsonObject state, EvccBridgeHandler bridgeHandler) { List results = new ArrayList<>(); - JsonObject statistics = state.getAsJsonObject(JSON_MEMBER_STATISTICS); + JsonObject statistics = state.getAsJsonObject(JSON_KEY_STATISTICS); if (statistics == null) { return results; } ThingUID uid = new ThingUID(EvccBindingConstants.THING_TYPE_STATISTICS, bridgeHandler.getThing().getUID(), - "statistics"); + JSON_KEY_STATISTICS); DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel("Statistics") .withBridge(bridgeHandler.getThing().getUID()).withProperty(PROPERTY_TYPE, PROPERTY_TYPE_STATISTICS) .withRepresentationProperty(PROPERTY_TYPE).build(); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/VehicleDiscoveryMapper.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/VehicleDiscoveryMapper.java index 98bea915c5845..fda872df8291f 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/VehicleDiscoveryMapper.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/VehicleDiscoveryMapper.java @@ -12,10 +12,7 @@ */ package org.openhab.binding.evcc.internal.discovery.mapper; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.JSON_MEMBER_VEHICLES; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.PROPERTY_ID; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.PROPERTY_TYPE; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.PROPERTY_TYPE_VEHICLE; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.*; import java.util.ArrayList; import java.util.Collection; @@ -44,14 +41,14 @@ public class VehicleDiscoveryMapper implements EvccDiscoveryMapper { @Override public Collection discover(JsonObject state, EvccBridgeHandler bridgeHandler) { List results = new ArrayList<>(); - JsonObject vehicles = state.getAsJsonObject(JSON_MEMBER_VEHICLES); + JsonObject vehicles = state.getAsJsonObject(JSON_KEY_VEHICLES); if (vehicles == null) { return results; } for (Map.Entry entry : vehicles.entrySet()) { JsonObject v = entry.getValue().getAsJsonObject(); String id = entry.getKey(); - String title = v.has("title") ? v.get("title").getAsString() : id; + String title = v.has(JSON_KEY_TITLE) ? v.get(JSON_KEY_TITLE).getAsString() : id; ThingUID uid = new ThingUID(EvccBindingConstants.THING_TYPE_VEHICLE, bridgeHandler.getThing().getUID(), Utils.sanitizeName(title)); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java index fd02446e48bf9..58ddbe2b12725 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java @@ -20,6 +20,9 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -87,8 +90,7 @@ public EvccBaseThingHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry } protected void commonInitialize(JsonObject state) { - Utils.sortJsonInPlace(state); - ThingBuilder builder = editThing(); + List newChannels = new ArrayList<>(); for (Map.Entry<@Nullable String, @Nullable JsonElement> entry : state.entrySet()) { String key = entry.getKey(); @@ -103,10 +105,15 @@ protected void commonInitialize(JsonObject state) { continue; } - createChannel(thingKey, builder, value); + @Nullable + Channel channel = createChannel(thingKey, value); + if (null != channel) { + newChannels.add(channel); + } } - updateThing(builder.build()); + newChannels.sort(Comparator.comparing(channel -> channel.getUID().getId())); + updateThing(editThing().withChannels(newChannels).build()); updateStatus(ThingStatus.ONLINE); isInitialized = true; Optional.ofNullable(bridgeHandler).ifPresentOrElse(handler -> handler.register(this), @@ -146,7 +153,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - protected void createChannel(String thingKey, ThingBuilder builder, JsonElement value) { + @Nullable + protected Channel createChannel(String thingKey, JsonElement value) { ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, thingKey); ItemTypeUnit typeUnit = getItemType(channelTypeUID); String itemType = typeUnit.itemType; @@ -156,12 +164,14 @@ protected void createChannel(String thingKey, ThingBuilder builder, JsonElement Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), thingKey)).withLabel(label) .withType(channelTypeUID).withAcceptedItemType(itemType).build(); if (getThing().getChannels().stream().noneMatch(c -> c.getUID().equals(channel.getUID()))) { - builder.withChannel(channel); + return channel; + // builder.withChannel(channel); } } else { String valString = Objects.requireNonNullElse(value.toString(), "Null"); logUnknownChannelXmlAsync(thingKey, "Hint for type: " + valString); } + return null; } private String getChannelLabel(String thingKey) { @@ -255,7 +265,6 @@ public void updateStatesFromApiResponse(JsonObject state) { return; } - Utils.sortJsonInPlace(state); for (Map.Entry<@Nullable String, @Nullable JsonElement> entry : state.entrySet()) { String key = entry.getKey(); JsonElement value = entry.getValue(); @@ -268,8 +277,15 @@ public void updateStatesFromApiResponse(JsonObject state) { Channel existingChannel = getThing().getChannel(channelUID.getId()); if (existingChannel == null) { ThingBuilder builder = editThing(); - createChannel(thingKey, builder, value); - updateThing(builder.build()); + List channels = getThing().getChannels(); + builder.withoutChannels(channels); + @Nullable + Channel newChannel = createChannel(thingKey, value); + if (null != newChannel) { + channels.add(newChannel); + channels.sort(Comparator.comparing(channel -> channel.getUID().getId())); + updateThing(builder.withChannels(channels).build()); + } } setItemValue(getItemType(new ChannelTypeUID(BINDING_ID, channelUID.getId())), channelUID, value); } 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 c768cdddfceda..9c04d379dcc7c 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 @@ -52,20 +52,20 @@ public void initialize() { return; } - JsonObject state = stateOpt.getAsJsonArray(JSON_MEMBER_BATTERY).get(index).getAsJsonObject(); + JsonObject state = stateOpt.getAsJsonArray(JSON_KEY_BATTERY).get(index).getAsJsonObject(); commonInitialize(state); }); } @Override public void prepareApiResponseForChannelStateUpdate(JsonObject state) { - state = state.has(JSON_MEMBER_BATTERY) ? state.getAsJsonArray(JSON_MEMBER_BATTERY).get(index).getAsJsonObject() + state = state.has(JSON_KEY_BATTERY) ? state.getAsJsonArray(JSON_KEY_BATTERY).get(index).getAsJsonObject() : new JsonObject(); super.updateStatesFromApiResponse(state); } @Override public JsonObject getStateFromCachedState(JsonObject state) { - return state.getAsJsonArray(JSON_MEMBER_BATTERY).get(index).getAsJsonObject(); + return state.getAsJsonArray(JSON_KEY_BATTERY).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 6c2d9945fb2e1..54e486b692b6a 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 @@ -12,7 +12,7 @@ */ package org.openhab.binding.evcc.internal.handler; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.JSON_MEMBER_LOADPOINTS; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.JSON_KEY_LOADPOINTS; import java.util.Map; @@ -75,9 +75,9 @@ public void prepareApiResponseForChannelStateUpdate(JsonObject state) { } protected void updateJSON(JsonObject state) { - JsonObject heatingState = state.getAsJsonArray(JSON_MEMBER_LOADPOINTS).get(index).getAsJsonObject(); + JsonObject heatingState = state.getAsJsonArray(JSON_KEY_LOADPOINTS).get(index).getAsJsonObject(); renameJsonKeys(heatingState); // rename the json keys - state.getAsJsonArray(JSON_MEMBER_LOADPOINTS).set(index, heatingState); // Update the keys in the original JSON + state.getAsJsonArray(JSON_KEY_LOADPOINTS).set(index, heatingState); // Update the keys in the original JSON } private static void renameJsonKeys(JsonObject json) { diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java index 2d9f99ae2a581..3227d64b69b0c 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java @@ -44,9 +44,11 @@ public class EvccLoadpointHandler extends EvccBaseThingHandler { private final Logger logger = LoggerFactory.getLogger(EvccLoadpointHandler.class); // JSON keys that need a special treatment, in example for backwards compatibility - private static final Map JSON_KEYS = Map.ofEntries(Map.entry("chargeCurrent", "offeredCurrent"), - Map.entry("vehiclePresent", "connected"), Map.entry("enabled", "charging"), - Map.entry("phases", "phasesConfigured"), Map.entry("chargeCurrents", "")); + private static final Map JSON_KEYS = Map.ofEntries( + Map.entry(JSON_KEY_CHARGE_CURRENT, JSON_KEY_OFFERED_CURRENT), + Map.entry(JSON_KEY_VEHICLE_PRESENT, JSON_KEY_CONNECTED), Map.entry(JSON_KEY_ENABLED, JSON_KEY_CHARGING), + Map.entry(JSON_KEY_PHASES, JSON_KEY_PHASES_CONFIGURED), Map.entry(JSON_KEY_CHARGE_CURRENTS, ""), + Map.entry(JSON_KEY_CHARGE_VOLTAGES, "")); protected final int index; private int[] version = {}; @@ -72,7 +74,7 @@ public void initialize() { heating.updateJSON(stateOpt.getAsJsonObject()); } - JsonObject state = stateOpt.getAsJsonArray(JSON_MEMBER_LOADPOINTS).get(index).getAsJsonObject(); + JsonObject state = stateOpt.getAsJsonArray(JSON_KEY_LOADPOINTS).get(index).getAsJsonObject(); modifyJSON(state); commonInitialize(state); @@ -84,8 +86,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof State) { String datapoint = Utils.getKeyFromChannelUID(channelUID).toLowerCase(); // Backwards compatibility for phasesConfigured - if ("configuredPhases".equals(datapoint) && version[0] == 0 && version[1] < 200) { - datapoint = "phases"; + if (JSON_KEY_PHASES_CONFIGURED.equals(datapoint) && version[0] == 0 && version[1] < 200) { + datapoint = JSON_KEY_PHASES; } // Special Handling for enable and disable endpoints if (datapoint.contains("enable")) { @@ -113,7 +115,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { @Override public void prepareApiResponseForChannelStateUpdate(JsonObject state) { version = Utils.convertVersionStringToIntArray(state.get("version").getAsString().split(" ")[0]); - state = state.getAsJsonArray(JSON_MEMBER_LOADPOINTS).get(index).getAsJsonObject(); + state = state.getAsJsonArray(JSON_KEY_LOADPOINTS).get(index).getAsJsonObject(); modifyJSON(state); super.updateStatesFromApiResponse(state); } @@ -121,9 +123,9 @@ public void prepareApiResponseForChannelStateUpdate(JsonObject state) { private void modifyJSON(JsonObject state) { JSON_KEYS.forEach((oldKey, newKey) -> { if (state.has(oldKey)) { - if (oldKey.equals("chargeCurrents")) { + if (oldKey.equals(JSON_KEY_CHARGE_CURRENTS)) { addMeasurementDatapointsToState(state, state.getAsJsonArray(oldKey), "Current"); - } else if (oldKey.equals("chargeVoltages")) { + } else if (oldKey.equals(JSON_KEY_CHARGE_VOLTAGES)) { addMeasurementDatapointsToState(state, state.getAsJsonArray(oldKey), "Voltage"); } else { state.add(newKey, state.get(oldKey)); @@ -143,8 +145,7 @@ protected void addMeasurementDatapointsToState(JsonObject state, JsonArray value @Override public JsonObject getStateFromCachedState(JsonObject state) { - return state.has(JSON_MEMBER_LOADPOINTS) - ? state.getAsJsonArray(JSON_MEMBER_LOADPOINTS).get(index).getAsJsonObject() + return state.has(JSON_KEY_LOADPOINTS) ? state.getAsJsonArray(JSON_KEY_LOADPOINTS).get(index).getAsJsonObject() : new JsonObject(); } } diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java index 82d2be8573269..ed8b52659c825 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java @@ -52,20 +52,20 @@ public void initialize() { return; } - JsonObject state = stateOpt.getAsJsonArray(JSON_MEMBER_PV).get(index).getAsJsonObject(); + JsonObject state = stateOpt.getAsJsonArray(JSON_KEY_PV).get(index).getAsJsonObject(); commonInitialize(state); }); } @Override public void prepareApiResponseForChannelStateUpdate(JsonObject state) { - state = state.getAsJsonArray(JSON_MEMBER_PV).get(index).getAsJsonObject(); + state = state.getAsJsonArray(JSON_KEY_PV).get(index).getAsJsonObject(); super.updateStatesFromApiResponse(state); } @Override public JsonObject getStateFromCachedState(JsonObject state) { - return state.has(JSON_MEMBER_PV) ? state.getAsJsonArray(JSON_MEMBER_PV).get(index).getAsJsonObject() + return state.has(JSON_KEY_PV) ? state.getAsJsonArray(JSON_KEY_PV).get(index).getAsJsonObject() : new JsonObject(); } } diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java index e6de0be2c8ae0..a2489c58ea808 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java @@ -62,7 +62,7 @@ public JsonObject getStateFromCachedState(JsonObject state) { @Override public void prepareApiResponseForChannelStateUpdate(JsonObject state) { - state = state.has(JSON_MEMBER_STATISTICS) ? state.getAsJsonObject(JSON_MEMBER_STATISTICS) : new JsonObject(); + state = state.has(JSON_KEY_STATISTICS) ? state.getAsJsonObject(JSON_KEY_STATISTICS) : new JsonObject(); for (String statisticsKey : state.keySet()) { JsonObject statistic = state.getAsJsonObject(statisticsKey); logger.debug("Extracting statistics for {}", statisticsKey); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java index 696e6f9397b4a..6b00df5e729c0 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java @@ -67,7 +67,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { @Override public void prepareApiResponseForChannelStateUpdate(JsonObject state) { - state = state.getAsJsonObject(JSON_MEMBER_VEHICLES).getAsJsonObject(vehicleId); + state = state.getAsJsonObject(JSON_KEY_VEHICLES).getAsJsonObject(vehicleId); super.updateStatesFromApiResponse(state); } @@ -82,14 +82,14 @@ public void initialize() { return; } - JsonObject state = stateOpt.getAsJsonObject(JSON_MEMBER_VEHICLES).getAsJsonObject(vehicleId); + JsonObject state = stateOpt.getAsJsonObject(JSON_KEY_VEHICLES).getAsJsonObject(vehicleId); commonInitialize(state); }); } @Override public JsonObject getStateFromCachedState(JsonObject state) { - return state.has(JSON_MEMBER_VEHICLES) ? state.getAsJsonObject(JSON_MEMBER_VEHICLES).getAsJsonObject(vehicleId) + return state.has(JSON_KEY_VEHICLES) ? state.getAsJsonObject(JSON_KEY_VEHICLES).getAsJsonObject(vehicleId) : new JsonObject(); } } diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/BaseThingHandlerTestClass.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/BaseThingHandlerTestClass.java new file mode 100644 index 0000000000000..220cb21508e41 --- /dev/null +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/BaseThingHandlerTestClass.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.evcc.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * The {@link BaseThingHandlerTestClass} is responsible for creating a subclass for testing the BaseThingHandler + * implementation + * + * @author Marcel Goerentz - Initial contribution + */ +@NonNullByDefault +public class BaseThingHandlerTestClass extends EvccBaseThingHandler { + public boolean setItemValueCalled = false; + public boolean createChannelCalled = false; + public boolean updateThingCalled = false; + public boolean updateStatusCalled = false; + public boolean prepareApiResponseForChannelStateUpdateCalled = true; + public boolean logUnknownChannelXmlCalled = false; + public ThingStatus lastUpdatedStatus = ThingStatus.UNKNOWN; + public boolean updateStateCalled = false; + public State lastState = UnDefType.UNDEF; + public ChannelUID lastChannelUID = new ChannelUID("dummy:dummy:dummy:dummy"); + + public BaseThingHandlerTestClass(Thing thing, ChannelTypeRegistry registry) { + super(thing, registry); + } + + @Override + protected void updateThing(Thing thing) { + updateThingCalled = true; + } + + @Override + protected void updateStatus(ThingStatus status) { + lastUpdatedStatus = status; + updateStatusCalled = true; + } + + @Override + @Nullable + protected Channel createChannel(String thingKey, JsonElement value) { + createChannelCalled = true; + return super.createChannel(thingKey, value); + } + + @Override + protected void setItemValue(ItemTypeUnit itemTypeUnit, ChannelUID channelUID, JsonElement value) { + setItemValueCalled = true; + super.setItemValue(itemTypeUnit, channelUID, value); + } + + @Override + public void prepareApiResponseForChannelStateUpdate(JsonObject state) { + prepareApiResponseForChannelStateUpdateCalled = true; + super.updateStatesFromApiResponse(state); + } + + @Override + public JsonObject getStateFromCachedState(JsonObject state) { + return new JsonObject(); + } + + @Override + public void updateState(ChannelUID uid, State state) { + updateStateCalled = true; + lastState = state; + lastChannelUID = uid; + } + + // Make sure no files are getting created + @Override + protected void logUnknownChannelXml(String key, String itemType) { + logUnknownChannelXmlCalled = true; + } +} diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java index 48e8ba679f9a1..ca841d161bc8c 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java @@ -34,7 +34,7 @@ import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_TEMPERATURE; import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_TIME; -import java.util.Collections; +import java.util.ArrayList; import java.util.Map; import java.util.stream.Stream; @@ -61,15 +61,11 @@ import org.openhab.core.thing.type.ChannelType; import org.openhab.core.thing.type.ChannelTypeRegistry; import org.openhab.core.types.RefreshType; -import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; -import io.micrometer.common.lang.Nullable; - /** * The {@link EvccBaseThingHandlerTest} is responsible for testing the BaseThingHandler implementation * @@ -82,84 +78,18 @@ public class EvccBaseThingHandlerTest { private Thing thing = mock(Thing.class); @SuppressWarnings("null") - private ChannelTypeRegistry channelTypeRegistry = mock(ChannelTypeRegistry.class); - - @Nullable - private TestEvccBaseThingHandler handler = new TestEvccBaseThingHandler(thing, channelTypeRegistry); - - // Concrete subclass for testing - private static class TestEvccBaseThingHandler extends EvccBaseThingHandler { - public boolean setItemValueCalled = false; - public boolean createChannelCalled = false; - public boolean updateThingCalled = false; - public boolean updateStatusCalled = false; - public boolean prepareApiResponseForChannelStateUpdateCalled = true; - public boolean logUnknownChannelXmlCalled = false; - public ThingStatus lastUpdatedStatus = ThingStatus.UNKNOWN; - public boolean updateStateCalled = false; - public State lastState = UnDefType.UNDEF; - public ChannelUID lastChannelUID = new ChannelUID("dummy:dummy:dummy:dummy"); - - public TestEvccBaseThingHandler(Thing thing, ChannelTypeRegistry registry) { - super(thing, registry); - } - - @Override - protected void updateThing(Thing thing) { - updateThingCalled = true; - } - - @Override - protected void updateStatus(ThingStatus status) { - lastUpdatedStatus = status; - updateStatusCalled = true; - } + private final ChannelTypeRegistry channelTypeRegistry = mock(ChannelTypeRegistry.class); - @Override - protected void createChannel(String thingKey, ThingBuilder builder, JsonElement value) { - createChannelCalled = true; - } - - @Override - protected void setItemValue(ItemTypeUnit itemTypeUnit, ChannelUID channelUID, JsonElement value) { - setItemValueCalled = true; - super.setItemValue(itemTypeUnit, channelUID, value); - } - - @Override - public void prepareApiResponseForChannelStateUpdate(JsonObject state) { - prepareApiResponseForChannelStateUpdateCalled = true; - super.updateStatesFromApiResponse(state); - } - - @Override - public JsonObject getStateFromCachedState(JsonObject state) { - return new JsonObject(); - } - - @Override - public void updateState(ChannelUID uid, State state) { - updateStateCalled = true; - lastState = state; - lastChannelUID = uid; - } - - // Make sure no files are getting created - @Override - protected void logUnknownChannelXml(String key, String itemType) { - logUnknownChannelXmlCalled = true; - } - } + private BaseThingHandlerTestClass handler = new BaseThingHandlerTestClass(thing, channelTypeRegistry); @SuppressWarnings("null") @BeforeEach public void setUp() { thing = mock(Thing.class); - ChannelTypeRegistry channelTypeRegistry = mock(ChannelTypeRegistry.class); - handler = spy(new TestEvccBaseThingHandler(thing, channelTypeRegistry)); + handler = spy(new BaseThingHandlerTestClass(thing, channelTypeRegistry)); when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "battery")); - when(thing.getChannels()).thenReturn(Collections.emptyList()); + when(thing.getChannels()).thenReturn(new ArrayList<>()); } @Nested @@ -191,6 +121,7 @@ public void updateFromEvccStateEmptyStateDoesNothing() { assertEquals(ThingStatus.UNKNOWN, handler.lastUpdatedStatus); } + @SuppressWarnings("null") @Test public void updateFromEvccStateWithPrimitiveValueCreatesChannelAndSetsItemValue() { handler.isInitialized = true; @@ -198,6 +129,9 @@ public void updateFromEvccStateWithPrimitiveValueCreatesChannelAndSetsItemValue( state.add("capacity", new JsonPrimitive(5.5)); // Channel does not exist when(thing.getChannel(anyString())).thenReturn(null); + ChannelType mockChannelType = mock(ChannelType.class); + when(mockChannelType.getItemType()).thenReturn(NUMBER_ENERGY); + when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType); handler.updateStatesFromApiResponse(state); assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); @@ -317,7 +251,7 @@ public void testCreateChannelWithUnknownItemType() { when(mockChannelType.getItemType()).thenReturn("Unknown"); when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType); - handler.createChannel("capacity", builder, value); + handler.createChannel("capacity", value); assertTrue(handler.createChannelCalled); verify(builder, never()).withChannel(any()); @@ -373,6 +307,7 @@ static Stream provideItemTypesWithExpectedStateClass() { Arguments.of(CoreItemFactory.SWITCH, OnOffType.class)); } + @SuppressWarnings("null") @ParameterizedTest @MethodSource("provideItemTypesWithExpectedStateClass") void setItemValueWithVariousTypes(String itemType, Class expectedStateClass) { From fd1205762aab3c89dcdf8d0ed6a788ddc06df6c4 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Tue, 2 Sep 2025 20:59:14 +0200 Subject: [PATCH 10/29] Remove utility method, fix typo in statistics.xml Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../binding/evcc/internal/handler/Utils.java | 46 ------------------- .../resources/OH-INF/i18n/evcc.properties | 2 +- .../resources/OH-INF/thing/statistics.xml | 2 +- 3 files changed, 2 insertions(+), 48 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java index 00b2d08e11392..f2d8f9612957a 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java @@ -126,50 +126,4 @@ public static int[] convertVersionStringToIntArray(String input) { } return Arrays.stream(input.split("\\.")).mapToInt(Integer::parseInt).toArray(); } - - /** - * Recursively sorts all JsonObjects in-place by their keys in alphabetical order. - * This method modifies the original JsonElement structure directly. - * It traverses through all nested JsonObjects and JsonArrays, and for each JsonObject, - * it reorders its keys alphabetically using a TreeMap. - * - * @param element The JsonElement to be sorted. Can be a JsonObject, JsonArray, or primitive. - */ - public static void sortJsonInPlace(JsonElement element) { - if (element.isJsonObject()) { - JsonObject obj = element.getAsJsonObject(); - - // Collect and sort entries by key - TreeMap sortedMap = new TreeMap<>(); - for (Map.Entry entry : obj.entrySet()) { - sortJsonInPlace(entry.getValue()); // Recursively sort child elements - sortedMap.put(entry.getKey(), entry.getValue()); - } - - // Clear and reinsert sorted entries - obj.entrySet().clear(); - for (Map.Entry entry : sortedMap.entrySet()) { - obj.add(entry.getKey(), entry.getValue()); - } - - } else if (element.isJsonArray()) { - for (JsonElement item : element.getAsJsonArray()) { - sortJsonInPlace(item); // Recursively sort array elements - } - } - // Primitive values remain unchanged - } - - /** - * Capitalizes the first character of a string. - * - * @param input The input string. - * @return The string with the first character in uppercase. - */ - public static String capitalizeFirstLetter(String input) { - if (input.isEmpty()) { - return input; - } - return input.substring(0, 1).toUpperCase() + input.substring(1); - } } diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties index 68ca34c1f6fb8..3ab1d5ed357f2 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties @@ -49,7 +49,7 @@ channel-group-type.evcc.statistics-group.label = Statistics Group # channel types -channel-type.evcc.avg-co2-type.label = Average C02 +channel-type.evcc.avg-co2-type.label = Average CO2 channel-type.evcc.avg-price-type.label = Average Price channel-type.evcc.battery-capacity.label = Battery Capacity channel-type.evcc.battery-capacity.description = Capacity of this battery diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/statistics.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/statistics.xml index 808abca29a182..44158112784f4 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/statistics.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/statistics.xml @@ -42,7 +42,7 @@ Number:EmissionIntensity - + Calculation Info From 526b942203f5a862e622085a764f78c3d3bb416b Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Wed, 3 Sep 2025 22:55:34 +0200 Subject: [PATCH 11/29] Add more tests Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../binding/evcc/internal/handler/Utils.java | 17 ++- .../AbstractThingHandlerTestClass.java | 86 ++++++++++++++ .../handler/BaseThingHandlerTestClass.java | 5 + .../handler/EvccBaseThingHandlerTest.java | 89 +++++++++++++-- .../handler/EvccBatteryHandlerTest.java | 105 ++++++++++++++++++ 5 files changed, 286 insertions(+), 16 deletions(-) create mode 100644 bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java create mode 100644 bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBatteryHandlerTest.java diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java index f2d8f9612957a..d7f706777d6e6 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java @@ -24,7 +24,6 @@ import java.util.Map; import java.util.Objects; import java.util.StringJoiner; -import java.util.TreeMap; import javax.measure.Unit; @@ -33,9 +32,6 @@ import org.openhab.core.library.unit.Units; import org.openhab.core.thing.ChannelUID; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - /** * The {@link Utils} provides utility methods * @@ -126,4 +122,17 @@ public static int[] convertVersionStringToIntArray(String input) { } return Arrays.stream(input.split("\\.")).mapToInt(Integer::parseInt).toArray(); } + + /** + * Capitalizes the first character of a string. + * + * @param input The input string. + * @return The string with the first character in uppercase. + */ + public static String capitalizeFirstLetter(String input) { + if (input.isEmpty()) { + return input; + } + return input.substring(0, 1).toUpperCase() + input.substring(1); + } } diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java new file mode 100644 index 0000000000000..6a7c029558c45 --- /dev/null +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.evcc.internal.handler; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.type.ChannelTypeRegistry; + +import com.google.gson.JsonObject; + +/** + * Abstract base test for EvccBaseThingHandler implementations. + * Extend this class in your handler tests and override createHandler(). + * + * @author Marcel Goerentz - Initial contribution + */ +@SuppressWarnings("null") +@NonNullByDefault +public abstract class AbstractThingHandlerTestClass { + + protected Thing thing = mock(Thing.class); + protected ChannelTypeRegistry channelTypeRegistry = mock(ChannelTypeRegistry.class); + @Nullable + protected T handler = null; + + protected ThingStatus lastThingStatus = ThingStatus.UNKNOWN; + protected ThingStatusDetail lastThingStatusDetail = ThingStatusDetail.NONE; + + /** + * Implement this to provide a handler instance for testing. + */ + protected abstract T createHandler(); + + @BeforeEach + public void setUp() { + handler = createHandler(); + when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); + when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "battery")); + when(thing.getChannels()).thenReturn(new ArrayList<>()); + } + + @Nested + class InitializeTests { + + @Test + public void initializeWithoutBridgeHandler() { + handler.initialize(); + assertEquals(ThingStatus.OFFLINE, lastThingStatus); + assertEquals(ThingStatusDetail.BRIDGE_UNINITIALIZED, lastThingStatusDetail); + } + + @Test + public void initializeWithBridgeHandlerWithoutCachedState() { + EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); + handler.bridgeHandler = bridgeHandler; + when(bridgeHandler.getCachedEvccState()).thenReturn(new JsonObject()); + + handler.initialize(); + assertEquals(ThingStatus.OFFLINE, lastThingStatus); + assertEquals(ThingStatusDetail.COMMUNICATION_ERROR, lastThingStatusDetail); + } + } +} diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/BaseThingHandlerTestClass.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/BaseThingHandlerTestClass.java index 220cb21508e41..606b3a469f8ce 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/BaseThingHandlerTestClass.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/BaseThingHandlerTestClass.java @@ -93,6 +93,11 @@ public void updateState(ChannelUID uid, State state) { // Make sure no files are getting created @Override protected void logUnknownChannelXml(String key, String itemType) { + } + + @Override + public void logUnknownChannelXmlAsync(String key, String itemType) { logUnknownChannelXmlCalled = true; + super.logUnknownChannelXmlAsync(key, itemType); } } diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java index ca841d161bc8c..d7c66c8e72bc2 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java @@ -12,17 +12,11 @@ */ package org.openhab.binding.evcc.internal.handler; -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 static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_CURRENCY; import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_DIMENSIONLESS; import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_ELECTRIC_CURRENT; @@ -36,6 +30,7 @@ import java.util.ArrayList; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -60,14 +55,16 @@ import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.type.ChannelType; import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import com.google.gson.JsonElement; +import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; /** - * The {@link EvccBaseThingHandlerTest} is responsible for testing the BaseThingHandler implementation + * The {@link EvccBaseThingHandlerTest} is responsible for testing the EvccBaseThingHandler implementation * * @author Marcel Goerentz - Initial contribution */ @@ -75,7 +72,7 @@ public class EvccBaseThingHandlerTest { @SuppressWarnings("null") - private Thing thing = mock(Thing.class); + private final Thing thing = mock(Thing.class); @SuppressWarnings("null") private final ChannelTypeRegistry channelTypeRegistry = mock(ChannelTypeRegistry.class); @@ -85,7 +82,6 @@ public class EvccBaseThingHandlerTest { @SuppressWarnings("null") @BeforeEach public void setUp() { - thing = mock(Thing.class); handler = spy(new BaseThingHandlerTestClass(thing, channelTypeRegistry)); when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "battery")); @@ -178,6 +174,19 @@ public void updateFromEvccStateSkipsNonPrimitiveValues() { assertTrue(handler.updateStatusCalled); // Status is updated even if nothing else happens assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); } + + @Test + void updateStatesFromApiResponseWithNullValueDoesNothing() { + handler.isInitialized = true; + JsonObject state = new JsonObject(); + state.add("capacity", null); // Null value + handler.updateStatesFromApiResponse(state); + assertFalse(handler.createChannelCalled); + assertFalse(handler.setItemValueCalled); + assertFalse(handler.updateThingCalled); + assertTrue(handler.updateStatusCalled); + assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); + } } @Nested @@ -238,6 +247,15 @@ public void handleCommandWithRefreshTypeAndMissingValue() { assertFalse(handler.setItemValueCalled); assertFalse(handler.logUnknownChannelXmlCalled); } + + @SuppressWarnings("null") + @Test + void handleCommandWithNonRefreshTypeDoesNothing() { + ChannelUID channelUID = new ChannelUID("test:thing:uid:battery-capacity"); + Command command = mock(org.openhab.core.types.Command.class); + handler.handleCommand(channelUID, command); + assertFalse(handler.setItemValueCalled); + } } @Test @@ -330,5 +348,52 @@ void setItemValueWithVariousTypes(String itemType, Class expectedStateClass) assertEquals(channelUID, handler.lastChannelUID); assertEquals(expectedStateClass, handler.lastState.getClass()); } + + @SuppressWarnings("null") + @Test + void setItemValueWithUnknownItemTypeDoesNotUpdateState() { + ChannelUID channelUID = new ChannelUID("test:thing:uid:dummy"); + JsonElement value = new JsonPrimitive(12.5); + ChannelType mockChannelType = mock(ChannelType.class); + when(mockChannelType.getItemType()).thenReturn("Unknown"); + ItemTypeUnit itemTypeUnit = new ItemTypeUnit(mockChannelType, Units.ONE); + + handler.setItemValue(itemTypeUnit, channelUID, value); + + assertFalse(handler.updateStateCalled); + assertTrue(handler.logUnknownChannelXmlCalled); + } + + @SuppressWarnings("null") + @Test + void setItemValueWithJsonNullDoesNotUpdateState() { + ChannelUID channelUID = new ChannelUID("test:thing:uid:dummy"); + JsonElement value = JsonNull.INSTANCE; + ChannelType mockChannelType = mock(ChannelType.class); + when(mockChannelType.getItemType()).thenReturn(CoreItemFactory.NUMBER); + ItemTypeUnit itemTypeUnit = new ItemTypeUnit(mockChannelType, Units.ONE); + + handler.setItemValue(itemTypeUnit, channelUID, value); + + assertFalse(handler.updateStateCalled); + } + } + + @SuppressWarnings("null") + @Test + void logUnknownChannelXmlAsyncIsCalledAsynchronously() { + CompletableFuture future = new CompletableFuture<>(); + doAnswer(invocation -> { + future.complete(true); + return null; + }).when(handler).logUnknownChannelXml(anyString(), anyString()); + + handler.logUnknownChannelXmlAsync("testKey", "testType"); + + try { + future.get(2, SECONDS); + } catch (Exception e) { + fail("logUnknownChannelXml was not called asynchronously"); + } } } 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 new file mode 100644 index 0000000000000..772f43457e3f3 --- /dev/null +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBatteryHandlerTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.evcc.internal.handler; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +/** + * The {@link EvccBatteryHandlerTest} is responsible for testing the EvccBatteryHandler implementation + * + * @author Marcel Goerentz - Initial contribution + */ +@NonNullByDefault +public class EvccBatteryHandlerTest extends AbstractThingHandlerTestClass { + + @Override + protected EvccBatteryHandler createHandler() { + return new EvccBatteryHandler(thing, channelTypeRegistry) { + protected void updateStatus(ThingStatus status, ThingStatusDetail detail) { + lastThingStatus = status; + lastThingStatusDetail = detail; + } + + protected void updateStatus(ThingStatus status) { + lastThingStatus = status; + } + + public void logUnknownChannelXmlAsync(String key, String itemType) { + } + }; + } + + @SuppressWarnings("null") + @Test + public void testInitializeWithBridgeHandlerWithValidState() { + EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); + handler.bridgeHandler = bridgeHandler; + + JsonObject batteryState = new JsonObject(); + batteryState.addProperty("soc", 50); + JsonArray batteryArray = new JsonArray(); + batteryArray.add(batteryState); + JsonObject state = new JsonObject(); + state.add("battery", batteryArray); + + when(bridgeHandler.getCachedEvccState()).thenReturn(state); + + handler.initialize(); + assertSame(ThingStatus.ONLINE, lastThingStatus); + } + + @SuppressWarnings("null") + @Test + public void testPrepareApiResponseForChannelStateUpdateIsInitialized() { + EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); + handler.bridgeHandler = bridgeHandler; + handler.isInitialized = true; + + JsonObject batteryState = new JsonObject(); + batteryState.addProperty("soc", 50); + JsonArray batteryArray = new JsonArray(); + batteryArray.add(batteryState); + JsonObject state = new JsonObject(); + state.add("battery", batteryArray); + + handler.prepareApiResponseForChannelStateUpdate(state); + assertSame(ThingStatus.ONLINE, lastThingStatus); + } + + @SuppressWarnings("null") + @Test + public void testPrepareApiResponseForChannelStateUpdateIsNotInitialized() { + EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); + handler.bridgeHandler = bridgeHandler; + + JsonObject batteryState = new JsonObject(); + batteryState.addProperty("soc", 50); + JsonArray batteryArray = new JsonArray(); + batteryArray.add(batteryState); + JsonObject state = new JsonObject(); + state.add("battery", batteryArray); + + handler.prepareApiResponseForChannelStateUpdate(state); + assertSame(ThingStatus.UNKNOWN, lastThingStatus); + } +} From a9cb2ed5513d6294950f2e08a90902a2e5aa8f1b Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Thu, 4 Sep 2025 15:32:21 +0200 Subject: [PATCH 12/29] Add tests for Heating, Loadpoint, PV and Site handler Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../internal/handler/EvccBatteryHandler.java | 2 +- .../internal/handler/EvccHeatingHandler.java | 2 +- .../handler/EvccLoadpointHandler.java | 8 +- .../evcc/internal/handler/EvccPvHandler.java | 2 +- .../internal/handler/EvccSiteHandler.java | 2 +- .../internal/handler/EvccVehicleHandler.java | 2 +- .../AbstractThingHandlerTestClass.java | 2 +- .../handler/EvccBatteryHandlerTest.java | 39 ++++- .../handler/EvccHeatingHandlerTest.java | 149 ++++++++++++++++ .../handler/EvccLoadpointHandlerTest.java | 164 ++++++++++++++++++ .../internal/handler/EvccPvHandlerTest.java | 130 ++++++++++++++ .../internal/handler/EvccSiteHandlerTest.java | 162 +++++++++++++++++ 12 files changed, 650 insertions(+), 14 deletions(-) create mode 100644 bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandlerTest.java create mode 100644 bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java create mode 100644 bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java create mode 100644 bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandlerTest.java 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 9c04d379dcc7c..b13607807fe20 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 @@ -61,7 +61,7 @@ public void initialize() { public void prepareApiResponseForChannelStateUpdate(JsonObject state) { state = state.has(JSON_KEY_BATTERY) ? state.getAsJsonArray(JSON_KEY_BATTERY).get(index).getAsJsonObject() : new JsonObject(); - super.updateStatesFromApiResponse(state); + updateStatesFromApiResponse(state); } @Override 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 54e486b692b6a..0f230c90e2b04 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 @@ -71,7 +71,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { @Override public void prepareApiResponseForChannelStateUpdate(JsonObject state) { updateJSON(state); - super.updateStatesFromApiResponse(state); + updateStatesFromApiResponse(state); } protected void updateJSON(JsonObject state) { diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java index 3227d64b69b0c..e902ac70c3478 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java @@ -117,16 +117,16 @@ public void prepareApiResponseForChannelStateUpdate(JsonObject state) { version = Utils.convertVersionStringToIntArray(state.get("version").getAsString().split(" ")[0]); state = state.getAsJsonArray(JSON_KEY_LOADPOINTS).get(index).getAsJsonObject(); modifyJSON(state); - super.updateStatesFromApiResponse(state); + updateStatesFromApiResponse(state); } private void modifyJSON(JsonObject state) { JSON_KEYS.forEach((oldKey, newKey) -> { if (state.has(oldKey)) { if (oldKey.equals(JSON_KEY_CHARGE_CURRENTS)) { - addMeasurementDatapointsToState(state, state.getAsJsonArray(oldKey), "Current"); + addMeasurementDatapointToState(state, state.getAsJsonArray(oldKey), "Current"); } else if (oldKey.equals(JSON_KEY_CHARGE_VOLTAGES)) { - addMeasurementDatapointsToState(state, state.getAsJsonArray(oldKey), "Voltage"); + addMeasurementDatapointToState(state, state.getAsJsonArray(oldKey), "Voltage"); } else { state.add(newKey, state.get(oldKey)); } @@ -135,7 +135,7 @@ private void modifyJSON(JsonObject state) { }); } - protected void addMeasurementDatapointsToState(JsonObject state, JsonArray values, String datapoint) { + protected void addMeasurementDatapointToState(JsonObject state, JsonArray values, String datapoint) { int phase = 1; for (JsonElement value : values) { state.add("charge" + datapoint + "L" + phase, value); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java index ed8b52659c825..55791a2490ae6 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java @@ -60,7 +60,7 @@ public void initialize() { @Override public void prepareApiResponseForChannelStateUpdate(JsonObject state) { state = state.getAsJsonArray(JSON_KEY_PV).get(index).getAsJsonObject(); - super.updateStatesFromApiResponse(state); + updateStatesFromApiResponse(state); } @Override 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 47981fd3acf3e..8104840bd35a5 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 @@ -71,7 +71,7 @@ public void prepareApiResponseForChannelStateUpdate(JsonObject state) { if (state.has("gridConfigured")) { modifyJSON(state); } - super.updateStatesFromApiResponse(state); + updateStatesFromApiResponse(state); } @Override diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java index 6b00df5e729c0..e742446a9291c 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java @@ -68,7 +68,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { @Override public void prepareApiResponseForChannelStateUpdate(JsonObject state) { state = state.getAsJsonObject(JSON_KEY_VEHICLES).getAsJsonObject(vehicleId); - super.updateStatesFromApiResponse(state); + updateStatesFromApiResponse(state); } @Override diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java index 6a7c029558c45..fa943688dd756 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java @@ -56,7 +56,7 @@ public abstract class AbstractThingHandlerTestClass()); 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 772f43457e3f3..ea5b33afc4bda 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 @@ -14,10 +14,14 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.Test; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; @@ -35,17 +39,31 @@ public class EvccBatteryHandlerTest extends AbstractThingHandlerTestClass { + + private final JsonObject testState = new JsonObject(); + private final JsonObject verifyObject = new JsonObject(); + + @Override + protected EvccHeatingHandler createHandler() { + return new EvccHeatingHandler(thing, channelTypeRegistry) { + + @Override + protected void updateStatus(ThingStatus status, ThingStatusDetail detail) { + lastThingStatus = status; + lastThingStatusDetail = detail; + } + + @Override + protected void updateStatus(ThingStatus status) { + lastThingStatus = status; + } + + @Override + public void logUnknownChannelXmlAsync(String key, String itemType) { + } + + @Nullable + @Override + protected Bridge getBridge() { + return null; + } + + @Override + public void updateThing(Thing thing) { + } + }; + } + + @BeforeEach + public void setup() { + handler = spy(createHandler()); + when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); + when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "heating")); + when(thing.getChannels()).thenReturn(new ArrayList<>()); + + verifyObject.addProperty("chargedEnergy", 50); + verifyObject.addProperty("effectiveLimitTemperature", 60); + verifyObject.addProperty("vehicleTemperature", 90); + verifyObject.addProperty("limitTemperature", 80); + verifyObject.addProperty("effectivePlanTemperature", 70); + verifyObject.addProperty("vehicleLimitTemperature", 90); + + JsonObject testObject = new JsonObject(); + testObject.addProperty("chargedEnergy", 50); + testObject.addProperty("effectiveLimitSoc", 60); + testObject.addProperty("effectivePlanSoc", 70); + testObject.addProperty("limitSoc", 80); + testObject.addProperty("vehicleLimitSoc", 90); + testObject.addProperty("vehicleSoc", 90); + JsonArray loadpointArray = new JsonArray(); + loadpointArray.add(testObject); + testState.add("loadpoints", loadpointArray); + } + + @SuppressWarnings("null") + @Test + public void testInitializeWithBridgeHandlerWithValidState() { + EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); + handler.bridgeHandler = bridgeHandler; + + when(bridgeHandler.getCachedEvccState()).thenReturn(testState); + + handler.initialize(); + assertSame(ThingStatus.ONLINE, lastThingStatus); + } + + @SuppressWarnings("null") + @Test + public void testPrepareApiResponseForChannelStateUpdateIsInitialized() { + handler.bridgeHandler = mock(EvccBridgeHandler.class); + handler.isInitialized = true; + + handler.prepareApiResponseForChannelStateUpdate(testState); + assertSame(ThingStatus.ONLINE, lastThingStatus); + } + + @SuppressWarnings("null") + @Test + public void testPrepareApiResponseForChannelStateUpdateIsNotInitialized() { + handler.bridgeHandler = mock(EvccBridgeHandler.class); + + handler.prepareApiResponseForChannelStateUpdate(testState); + assertSame(ThingStatus.UNKNOWN, lastThingStatus); + } + + @SuppressWarnings("null") + @Test + public void testJsonGetsModifiedCorrectly() { + handler.prepareApiResponseForChannelStateUpdate(testState); + assertEquals(verifyObject, testState.getAsJsonArray("loadpoints").get(0)); + } + + @SuppressWarnings("null") + @Test + public void testGetStateFromCachedState() { + JsonObject result = handler.getStateFromCachedState(testState); + assertSame(testState.getAsJsonArray("loadpoints").get(0), result); + } +} diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java new file mode 100644 index 0000000000000..427ad37869161 --- /dev/null +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +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.openhab.binding.evcc.internal.EvccBindingConstants.*; + +import java.util.ArrayList; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingUID; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +/** + * The {@link EvccLoadpointHandlerTest} is responsible for testing the EvccLoadpointHandler implementation + * + * @author Marcel Goerentz - Initial contribution + */ +@NonNullByDefault +public class EvccLoadpointHandlerTest extends AbstractThingHandlerTestClass { + + private final JsonObject testState = new JsonObject(); + private final JsonObject testObject = new JsonObject(); + private final JsonObject verifyObject = new JsonObject(); + + @Override + protected EvccLoadpointHandler createHandler() { + return new EvccLoadpointHandler(thing, channelTypeRegistry) { + + @Override + protected void updateStatus(ThingStatus status, ThingStatusDetail detail) { + lastThingStatus = status; + lastThingStatusDetail = detail; + } + + @Override + protected void updateStatus(ThingStatus status) { + lastThingStatus = status; + } + + @Override + public void logUnknownChannelXmlAsync(String key, String itemType) { + } + + @Nullable + @Override + protected Bridge getBridge() { + return null; + } + + @Override + public void updateThing(Thing thing) { + } + }; + } + + @BeforeEach + public void setup() { + handler = spy(createHandler()); + when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); + when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "loadpoint")); + when(thing.getChannels()).thenReturn(new ArrayList<>()); + + verifyObject.addProperty("chargedEnergy", 50); + verifyObject.addProperty(JSON_KEY_OFFERED_CURRENT, 6); + verifyObject.addProperty(JSON_KEY_CONNECTED, true); + verifyObject.addProperty(JSON_KEY_CHARGING, true); + verifyObject.addProperty(JSON_KEY_PHASES_CONFIGURED, "3"); + verifyObject.addProperty("chargeCurrentL1", 6); + verifyObject.addProperty("chargeCurrentL2", 7); + verifyObject.addProperty("chargeCurrentL3", 8); + verifyObject.addProperty("chargeVoltageL1", 230.0); + verifyObject.addProperty("chargeVoltageL2", 231.0); + verifyObject.addProperty("chargeVoltageL3", 229.0); + + testObject.addProperty("chargedEnergy", 50); + testObject.addProperty(JSON_KEY_CHARGE_CURRENT, 6); + testObject.addProperty(JSON_KEY_VEHICLE_PRESENT, true); + testObject.addProperty(JSON_KEY_ENABLED, true); + testObject.addProperty(JSON_KEY_PHASES, "3"); + JsonArray currents = new JsonArray(); + currents.add(6); + currents.add(7); + currents.add(8); + testObject.add(JSON_KEY_CHARGE_CURRENTS, currents); + JsonArray voltages = new JsonArray(); + voltages.add(230.0); + voltages.add(231.0); + voltages.add(229.0); + testObject.add(JSON_KEY_CHARGE_VOLTAGES, voltages); + JsonArray loadpointArray = new JsonArray(); + loadpointArray.add(testObject); + testState.add("loadpoints", loadpointArray); + testState.addProperty("version", "0.207.0"); + } + + @SuppressWarnings("null") + @Test + public void testInitializeWithBridgeHandlerWithValidState() { + EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); + handler.bridgeHandler = bridgeHandler; + when(bridgeHandler.getCachedEvccState()).thenReturn(testState); + + handler.initialize(); + assertSame(ThingStatus.ONLINE, lastThingStatus); + } + + @SuppressWarnings("null") + @Test + public void testPrepareApiResponseForChannelStateUpdateIsInitialized() { + handler.bridgeHandler = mock(EvccBridgeHandler.class); + handler.isInitialized = true; + + handler.prepareApiResponseForChannelStateUpdate(testState); + assertSame(ThingStatus.ONLINE, lastThingStatus); + } + + @SuppressWarnings("null") + @Test + public void testPrepareApiResponseForChannelStateUpdateIsNotInitialized() { + handler.bridgeHandler = mock(EvccBridgeHandler.class); + + handler.prepareApiResponseForChannelStateUpdate(testState); + assertSame(ThingStatus.UNKNOWN, lastThingStatus); + } + + @SuppressWarnings("null") + @Test + public void testJsonGetsModifiedCorrectly() { + handler.prepareApiResponseForChannelStateUpdate(testState); + assertEquals(verifyObject, testState.getAsJsonArray("loadpoints").get(0)); + } + + @SuppressWarnings("null") + @Test + public void testGetStateFromCachedState() { + JsonObject result = handler.getStateFromCachedState(testState); + assertSame(testState.getAsJsonArray("loadpoints").get(0), result); + } +} diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java new file mode 100644 index 0000000000000..fe60e920a0045 --- /dev/null +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.evcc.internal.handler; + +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 java.util.ArrayList; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingUID; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +/** + * The {@link EvccSiteHandlerTest} is responsible for testing the EvccPvHandler implementation + * + * @author Marcel Goerentz - Initial contribution + */ +@NonNullByDefault +public class EvccPvHandlerTest extends AbstractThingHandlerTestClass { + + private final JsonObject testState = new JsonObject(); + private final JsonObject testObject = new JsonObject(); + private final JsonObject verifyObject = new JsonObject(); + + @Override + protected EvccPvHandler createHandler() { + return new EvccPvHandler(thing, channelTypeRegistry) { + + @Override + protected void updateStatus(ThingStatus status, ThingStatusDetail detail) { + lastThingStatus = status; + lastThingStatusDetail = detail; + } + + @Override + protected void updateStatus(ThingStatus status) { + lastThingStatus = status; + } + + @Override + public void logUnknownChannelXmlAsync(String key, String itemType) { + } + + @Nullable + @Override + protected Bridge getBridge() { + return null; + } + + @Override + public void updateThing(Thing thing) { + } + }; + } + + @BeforeEach + public void setup() { + handler = spy(createHandler()); + when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); + when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "pv")); + when(thing.getChannels()).thenReturn(new ArrayList<>()); + + verifyObject.addProperty("power", 2000); + + testObject.addProperty("power", 2000); + JsonArray loadpointArray = new JsonArray(); + loadpointArray.add(testObject); + testState.add("pv", loadpointArray); + } + + @SuppressWarnings("null") + @Test + public void testInitializeWithBridgeHandlerWithValidState() { + EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); + handler.bridgeHandler = bridgeHandler; + when(bridgeHandler.getCachedEvccState()).thenReturn(testState); + + handler.initialize(); + assertSame(ThingStatus.ONLINE, lastThingStatus); + } + + @SuppressWarnings("null") + @Test + public void testPrepareApiResponseForChannelStateUpdateIsInitialized() { + handler.bridgeHandler = mock(EvccBridgeHandler.class); + handler.isInitialized = true; + + handler.prepareApiResponseForChannelStateUpdate(testState); + assertSame(ThingStatus.ONLINE, lastThingStatus); + } + + @SuppressWarnings("null") + @Test + public void testPrepareApiResponseForChannelStateUpdateIsNotInitialized() { + handler.bridgeHandler = mock(EvccBridgeHandler.class); + + handler.prepareApiResponseForChannelStateUpdate(testState); + assertSame(ThingStatus.UNKNOWN, lastThingStatus); + } + + @SuppressWarnings("null") + @Test + public void testGetStateFromCachedState() { + JsonObject result = handler.getStateFromCachedState(testState); + assertSame(testState.getAsJsonArray("pv").get(0), result); + } +} diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandlerTest.java new file mode 100644 index 0000000000000..88ee8d880776d --- /dev/null +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandlerTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +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 java.util.ArrayList; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingUID; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +/** + * The {@link EvccSiteHandlerTest} is responsible for testing the EvccSiteHandler implementation + * + * @author Marcel Goerentz - Initial contribution + */ +@NonNullByDefault +public class EvccSiteHandlerTest extends AbstractThingHandlerTestClass { + + private final JsonObject testState = new JsonObject(); + private final JsonObject verifyObject = new JsonObject(); + private final JsonObject gridConfigured = new JsonObject(); + + @Override + protected EvccSiteHandler createHandler() { + return new EvccSiteHandler(thing, channelTypeRegistry) { + + @Override + protected void updateStatus(ThingStatus status, ThingStatusDetail detail) { + lastThingStatus = status; + lastThingStatusDetail = detail; + } + + @Override + protected void updateStatus(ThingStatus status) { + lastThingStatus = status; + } + + @Override + public void logUnknownChannelXmlAsync(String key, String itemType) { + } + + @Nullable + @Override + protected Bridge getBridge() { + return null; + } + + @Override + public void updateThing(Thing thing) { + } + }; + } + + @BeforeEach + public void setup() { + handler = spy(createHandler()); + when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); + when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "pv")); + when(thing.getChannels()).thenReturn(new ArrayList<>()); + + verifyObject.addProperty("version", "0.207.0"); + verifyObject.addProperty("gridPower", 2000); + verifyObject.addProperty("gridEnergy", 10000); + verifyObject.addProperty("gridCurrentL1", 6); + verifyObject.addProperty("gridCurrentL2", 7); + verifyObject.addProperty("gridCurrentL3", 8); + verifyObject.addProperty("gridVoltageL1", 230.0); + verifyObject.addProperty("gridVoltageL2", 231.0); + verifyObject.addProperty("gridVoltageL3", 229.0); + + gridConfigured.addProperty("power", 2000); + gridConfigured.addProperty("energy", 10000); + JsonArray currents = new JsonArray(); + currents.add(6); + currents.add(7); + currents.add(8); + gridConfigured.add("currents", currents); + JsonArray voltages = new JsonArray(); + voltages.add(230.0); + voltages.add(231.0); + voltages.add(229.0); + gridConfigured.add("voltages", voltages); + testState.addProperty("version", "0.207.0"); + } + + @SuppressWarnings("null") + @Test + public void testInitializeWithBridgeHandlerWithValidState() { + EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); + handler.bridgeHandler = bridgeHandler; + when(bridgeHandler.getCachedEvccState()).thenReturn(testState); + + handler.initialize(); + assertSame(ThingStatus.ONLINE, lastThingStatus); + } + + @SuppressWarnings("null") + @Nested + public class TestPrepareApiResponseForChannelStateUpdate { + + @Test + public void handlerIsInitialized() { + handler.bridgeHandler = mock(EvccBridgeHandler.class); + handler.isInitialized = true; + + handler.prepareApiResponseForChannelStateUpdate(testState); + assertSame(ThingStatus.ONLINE, lastThingStatus); + } + + @Test + public void handlerIsNotInitialized() { + handler.bridgeHandler = mock(EvccBridgeHandler.class); + + handler.prepareApiResponseForChannelStateUpdate(testState); + assertSame(ThingStatus.UNKNOWN, lastThingStatus); + } + + @Test + public void stateContainsGridConfigured() { + handler.bridgeHandler = mock(EvccBridgeHandler.class); + + testState.addProperty("gridConfigured", true); + testState.add("grid", gridConfigured); + handler.prepareApiResponseForChannelStateUpdate(testState); + assertEquals(verifyObject, testState); + } + } + + @SuppressWarnings("null") + @Test + public void testGetStateFromCachedState() { + JsonObject result = handler.getStateFromCachedState(testState); + assertSame(testState, result); + } +} From 8a0e552d37de10f83df99a975663a92ce7e650ca Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Wed, 10 Sep 2025 09:05:22 +0200 Subject: [PATCH 13/29] Update tests, add example response and add a channel to loadpoints Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../evcc/internal/EvccBindingConstants.java | 30 +- .../internal/handler/EvccHeatingHandler.java | 10 +- .../handler/EvccLoadpointHandler.java | 2 +- .../internal/handler/EvccSiteHandler.java | 20 +- .../handler/EvccStatisticsHandler.java | 12 + .../resources/OH-INF/i18n/evcc.properties | 4 + .../main/resources/OH-INF/thing/loadpoint.xml | 15 + .../AbstractThingHandlerTestClass.java | 36 +- .../handler/EvccBatteryHandlerTest.java | 72 +- .../handler/EvccHeatingHandlerTest.java | 65 +- .../handler/EvccLoadpointHandlerTest.java | 55 +- .../internal/handler/EvccPvHandlerTest.java | 2 +- .../internal/handler/EvccSiteHandlerTest.java | 43 +- .../handler/EvccStatisticsHandlerTest.java | 110 ++ .../handler/EvccVehicleHandlerTest.java | 89 ++ .../resources/responses/example_response.json | 1243 +++++++++++++++++ 16 files changed, 1645 insertions(+), 163 deletions(-) create mode 100644 bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandlerTest.java create mode 100644 bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandlerTest.java create mode 100644 bundles/org.openhab.binding.evcc/src/test/resources/responses/example_response.json 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 4f4c44cec7887..48b52b503693c 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 @@ -60,22 +60,30 @@ public class EvccBindingConstants { public static final String PROPERTY_TYPE_VEHICLE = "vehicle"; public static final String JSON_KEY_BATTERY = "battery"; - public static final String JSON_KEY_LOADPOINTS = "loadpoints"; - public static final String JSON_KEY_PV = "pv"; - public static final String JSON_KEY_STATISTICS = "statistics"; - public static final String JSON_KEY_VEHICLES = "vehicles"; - public static final String JSON_KEY_CHARGER_FEATURE_HEATING = "chargerFeatureHeating"; - public static final String JSON_KEY_TITLE = "title"; public static final String JSON_KEY_CHARGE_CURRENT = "chargeCurrent"; - public static final String JSON_KEY_OFFERED_CURRENT = "offeredCurrent"; - public static final String JSON_KEY_VEHICLE_PRESENT = "vehiclePresent"; + public static final String JSON_KEY_CHARGE_CURRENTS = "chargeCurrents"; + public static final String JSON_KEY_CHARGE_VOLTAGES = "chargeVoltages"; + public static final String JSON_KEY_CHARGER_FEATURE_HEATING = "chargerFeatureHeating"; + public static final String JSON_KEY_CHARGING = "charging"; public static final String JSON_KEY_CONNECTED = "connected"; + public static final String JSON_KEY_EFFECTIVE_LIMIT_SOC = "effectiveLimitSoc"; + public static final String JSON_KEY_EFFECTIVE_PLAN_SOC = "effectivePlanSoc"; public static final String JSON_KEY_ENABLED = "enabled"; - public static final String JSON_KEY_CHARGING = "charging"; + public static final String JSON_KEY_GRID = "grid"; + public static final String JSON_KEY_GRID_CONFIGURED = "gridConfigured"; + public static final String JSON_KEY_LIMIT_SOC = "limitSoc"; + public static final String JSON_KEY_LOADPOINTS = "loadpoints"; + public static final String JSON_KEY_OFFERED_CURRENT = "offeredCurrent"; public static final String JSON_KEY_PHASES = "phases"; public static final String JSON_KEY_PHASES_CONFIGURED = "phasesConfigured"; - public static final String JSON_KEY_CHARGE_CURRENTS = "chargeCurrents"; - public static final String JSON_KEY_CHARGE_VOLTAGES = "chargeVoltages"; + public static final String JSON_KEY_PV = "pv"; + public static final String JSON_KEY_SMART_COST_TYPE = "smartCostType"; + public static final String JSON_KEY_STATISTICS = "statistics"; + public static final String JSON_KEY_TITLE = "title"; + public static final String JSON_KEY_VEHICLE_LIMIT_SOC = "vehicleLimitSoc"; + public static final String JSON_KEY_VEHICLE_PRESENT = "vehiclePresent"; + public static final String JSON_KEY_VEHICLE_SOC = "vehicleSoc"; + public static final String JSON_KEY_VEHICLES = "vehicles"; public static final String NUMBER_CURRENCY = CoreItemFactory.NUMBER + ":Currency"; public static final String NUMBER_DIMENSIONLESS = CoreItemFactory.NUMBER + ":Dimensionless"; 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 0f230c90e2b04..4ead6aef4c568 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 @@ -12,7 +12,7 @@ */ package org.openhab.binding.evcc.internal.handler; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.JSON_KEY_LOADPOINTS; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.*; import java.util.Map; @@ -38,9 +38,11 @@ public class EvccHeatingHandler extends EvccLoadpointHandler { private final Logger logger = LoggerFactory.getLogger(EvccHeatingHandler.class); private static final Map JSON_KEYS = Map.ofEntries( - Map.entry("effectiveLimitTemperature", "effectiveLimitSoc"), - Map.entry("effectivePlanTemperature", "effectivePlanSoc"), Map.entry("limitTemperature", "limitSoc"), - Map.entry("vehicleLimitTemperature", "vehicleLimitSoc"), Map.entry("vehicleTemperature", "vehicleSoc")); + 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/EvccLoadpointHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java index e902ac70c3478..d630e09c1d503 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java @@ -46,7 +46,7 @@ public class EvccLoadpointHandler extends EvccBaseThingHandler { // JSON keys that need a special treatment, in example for backwards compatibility private static final Map JSON_KEYS = Map.ofEntries( Map.entry(JSON_KEY_CHARGE_CURRENT, JSON_KEY_OFFERED_CURRENT), - Map.entry(JSON_KEY_VEHICLE_PRESENT, JSON_KEY_CONNECTED), Map.entry(JSON_KEY_ENABLED, JSON_KEY_CHARGING), + Map.entry(JSON_KEY_VEHICLE_PRESENT, JSON_KEY_CONNECTED), Map.entry(JSON_KEY_PHASES, JSON_KEY_PHASES_CONFIGURED), Map.entry(JSON_KEY_CHARGE_CURRENTS, ""), Map.entry(JSON_KEY_CHARGE_VOLTAGES, "")); protected final int index; 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 8104840bd35a5..9f271566f1c3e 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 @@ -12,6 +12,8 @@ */ package org.openhab.binding.evcc.internal.handler; +import static org.openhab.binding.evcc.internal.EvccBindingConstants.*; + import java.util.Map; import java.util.Optional; @@ -49,7 +51,7 @@ public EvccSiteHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) { public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof State) { String datapoint = Utils.getKeyFromChannelUID(channelUID).toLowerCase(); - String value = ""; + String value; if (command instanceof OnOffType) { value = command == OnOffType.ON ? "true" : "false"; } else { @@ -68,7 +70,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { @Override public void prepareApiResponseForChannelStateUpdate(JsonObject state) { - if (state.has("gridConfigured")) { + if (state.has(JSON_KEY_GRID_CONFIGURED)) { modifyJSON(state); } updateStatesFromApiResponse(state); @@ -86,11 +88,11 @@ public void initialize() { } // Set the smart cost type - if (state.has("smartCostType") && !state.get("smartCostType").isJsonNull()) { - smartCostType = state.get("smartCostType").getAsString(); + if (state.has(JSON_KEY_SMART_COST_TYPE) && !state.get(JSON_KEY_SMART_COST_TYPE).isJsonNull()) { + smartCostType = state.get(JSON_KEY_SMART_COST_TYPE).getAsString(); } - if (state.has("gridConfigured")) { + if (state.has(JSON_KEY_GRID_CONFIGURED)) { modifyJSON(state); } commonInitialize(state); @@ -98,17 +100,17 @@ public void initialize() { } private void modifyJSON(JsonObject state) { - for (Map.Entry entry : state.getAsJsonObject("grid").entrySet()) { + for (Map.Entry entry : state.getAsJsonObject(JSON_KEY_GRID).entrySet()) { if ("currents".equals(entry.getKey())) { addMeasurementDatapointsToState(state, entry.getValue().getAsJsonArray(), "Current"); } else if ("voltages".equals(entry.getKey())) { addMeasurementDatapointsToState(state, entry.getValue().getAsJsonArray(), "Voltage"); } else { - state.add("grid" + Utils.capitalizeFirstLetter(entry.getKey()), entry.getValue()); + state.add(JSON_KEY_GRID + Utils.capitalizeFirstLetter(entry.getKey()), entry.getValue()); } } - state.remove("grid"); - state.remove("gridConfigured"); + state.remove(JSON_KEY_GRID); + state.remove(JSON_KEY_GRID_CONFIGURED); } protected void addMeasurementDatapointsToState(JsonObject state, JsonArray values, String datapoint) { diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java index a2489c58ea808..50001bbc926dc 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java @@ -24,6 +24,7 @@ import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.type.ChannelTypeRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,6 +53,12 @@ public void initialize() { handler.register(this); updateStatus(ThingStatus.ONLINE); isInitialized = true; + JsonObject stateOpt = handler.getCachedEvccState().deepCopy(); + if (stateOpt.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + return; + } + prepareApiResponseForChannelStateUpdate(stateOpt); }); } @@ -63,6 +70,11 @@ public JsonObject getStateFromCachedState(JsonObject state) { @Override public void prepareApiResponseForChannelStateUpdate(JsonObject state) { state = state.has(JSON_KEY_STATISTICS) ? state.getAsJsonObject(JSON_KEY_STATISTICS) : new JsonObject(); + if (!isInitialized || state.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + return; + } + updateStatus(ThingStatus.ONLINE); for (String statisticsKey : state.keySet()) { JsonObject statistic = state.getAsJsonObject(statisticsKey); logger.debug("Extracting statistics for {}", statisticsKey); diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties index 3ab1d5ed357f2..f76510e1668de 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties @@ -151,6 +151,10 @@ channel-type.evcc.loadpoint-enable-delay.label = Enable Delay channel-type.evcc.loadpoint-enable-delay.description = Delay in PV mode before starting the charging when enough power is available channel-type.evcc.loadpoint-enable-threshold.label = Enable Threshold channel-type.evcc.loadpoint-enable-threshold.description = Threshold for the enable delay +channel-type.evcc.loadpoint-enabled.label = Enabled +channel-type.evcc.loadpoint-enabled.description = Indicating whether this loadpoint is enabled or not +channel-type.evcc.loadpoint-enabled.state.option.ON = Enabled +channel-type.evcc.loadpoint-enabled.state.option.OFF = Disabled channel-type.evcc.loadpoint-enabled.label = Loadpoint Enabled channel-type.evcc.loadpoint-enabled.description = Indicates whether the charger is locked or released channel-type.evcc.loadpoint-enabled.state.option.ON = Released diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml index 0777f30cb717f..4d55c804f5e6d 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml @@ -284,6 +284,21 @@ + + Switch + + Indicating whether this loadpoint is enabled or not + + Status + Enabled + + + + + + + + DateTime diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java index fa943688dd756..be75061bf7c5a 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java @@ -15,11 +15,15 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -30,6 +34,7 @@ import org.openhab.core.thing.type.ChannelTypeRegistry; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; /** * Abstract base test for EvccBaseThingHandler implementations. @@ -49,22 +54,41 @@ public abstract class AbstractThingHandlerTestClass()); + @BeforeAll + static void setUpOnce() { + try (InputStream is = EvccBatteryHandlerTest.class.getClassLoader() + .getResourceAsStream("responses/example_response.json")) { + if (is == null) { + throw new IllegalArgumentException("Couldn't find response file"); + } + String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); + exampleResponse = JsonParser.parseString(json).getAsJsonObject(); + verifyObject = exampleResponse.deepCopy(); + + } catch (IOException e) { + fail("Failed to read example response file", e); + } } @Nested class InitializeTests { + @BeforeEach + public void setUp() { + handler = spy(createHandler()); + when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); + when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "battery")); + when(thing.getChannels()).thenReturn(new ArrayList<>()); + } + @Test public void initializeWithoutBridgeHandler() { handler.initialize(); 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 ea5b33afc4bda..e9ff93007c2d7 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 @@ -13,19 +13,22 @@ package org.openhab.binding.evcc.internal.handler; import static org.junit.jupiter.api.Assertions.assertSame; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingUID; -import com.google.gson.JsonArray; import com.google.gson.JsonObject; /** @@ -36,6 +39,8 @@ @NonNullByDefault public class EvccBatteryHandlerTest extends AbstractThingHandlerTestClass { + private static JsonObject batteryState = new JsonObject(); + @Override protected EvccBatteryHandler createHandler() { return new EvccBatteryHandler(thing, channelTypeRegistry) { @@ -67,21 +72,27 @@ public void updateThing(Thing thing) { }; } + @BeforeAll + static void setUpOnce() { + batteryState = exampleResponse.getAsJsonArray("battery").get(0).getAsJsonObject(); + } + @SuppressWarnings("null") - @Test - public void testInitializeWithBridgeHandlerWithValidState() { + @BeforeEach + public void setup() { + when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); + when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "battery")); + when(thing.getChannels()).thenReturn(new ArrayList<>()); + handler = spy(createHandler()); + EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); handler.bridgeHandler = bridgeHandler; + when(bridgeHandler.getCachedEvccState()).thenReturn(exampleResponse); + } - JsonObject batteryState = new JsonObject(); - batteryState.addProperty("soc", 50); - JsonArray batteryArray = new JsonArray(); - batteryArray.add(batteryState); - JsonObject state = new JsonObject(); - state.add("battery", batteryArray); - - when(bridgeHandler.getCachedEvccState()).thenReturn(state); - + @SuppressWarnings("null") + @Test + public void testInitializeWithBridgeHandlerWithValidState() { handler.initialize(); assertSame(ThingStatus.ONLINE, lastThingStatus); } @@ -89,33 +100,15 @@ public void testInitializeWithBridgeHandlerWithValidState() { @SuppressWarnings("null") @Test public void testPrepareApiResponseForChannelStateUpdateIsInitialized() { - handler.bridgeHandler = mock(EvccBridgeHandler.class); handler.isInitialized = true; - - JsonObject batteryState = new JsonObject(); - batteryState.addProperty("soc", 50); - JsonArray batteryArray = new JsonArray(); - batteryArray.add(batteryState); - JsonObject state = new JsonObject(); - state.add("battery", batteryArray); - - handler.prepareApiResponseForChannelStateUpdate(state); + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); assertSame(ThingStatus.ONLINE, lastThingStatus); } @SuppressWarnings("null") @Test public void testPrepareApiResponseForChannelStateUpdateIsNotInitialized() { - handler.bridgeHandler = mock(EvccBridgeHandler.class); - - JsonObject batteryState = new JsonObject(); - batteryState.addProperty("soc", 50); - JsonArray batteryArray = new JsonArray(); - batteryArray.add(batteryState); - JsonObject state = new JsonObject(); - state.add("battery", batteryArray); - - handler.prepareApiResponseForChannelStateUpdate(state); + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); verify(handler).updateStatesFromApiResponse(batteryState); assertSame(ThingStatus.UNKNOWN, lastThingStatus); } @@ -123,14 +116,7 @@ public void testPrepareApiResponseForChannelStateUpdateIsNotInitialized() { @SuppressWarnings("null") @Test public void testGetStateFromCachedState() { - JsonObject batteryState = new JsonObject(); - batteryState.addProperty("soc", 50); - JsonArray batteryArray = new JsonArray(); - batteryArray.add(batteryState); - JsonObject state = new JsonObject(); - state.add("battery", batteryArray); - - JsonObject result = handler.getStateFromCachedState(state); + JsonObject result = handler.getStateFromCachedState(exampleResponse); assertSame(batteryState, result); } } diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandlerTest.java index 51abba7fc1171..c49c72dafd0da 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandlerTest.java @@ -14,9 +14,8 @@ 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 static org.openhab.binding.evcc.internal.EvccBindingConstants.*; import java.util.ArrayList; import java.util.Map; @@ -31,7 +30,6 @@ import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingUID; -import com.google.gson.JsonArray; import com.google.gson.JsonObject; /** @@ -42,8 +40,7 @@ @NonNullByDefault public class EvccHeatingHandlerTest extends AbstractThingHandlerTestClass { - private final JsonObject testState = new JsonObject(); - private final JsonObject verifyObject = new JsonObject(); + private JsonObject heatingObject = new JsonObject(); @Override protected EvccHeatingHandler createHandler() { @@ -76,40 +73,33 @@ public void updateThing(Thing thing) { }; } + @SuppressWarnings("null") @BeforeEach public void setup() { - handler = spy(createHandler()); when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "heating")); when(thing.getChannels()).thenReturn(new ArrayList<>()); - - verifyObject.addProperty("chargedEnergy", 50); - verifyObject.addProperty("effectiveLimitTemperature", 60); - verifyObject.addProperty("vehicleTemperature", 90); - verifyObject.addProperty("limitTemperature", 80); - verifyObject.addProperty("effectivePlanTemperature", 70); - verifyObject.addProperty("vehicleLimitTemperature", 90); - - JsonObject testObject = new JsonObject(); - testObject.addProperty("chargedEnergy", 50); - testObject.addProperty("effectiveLimitSoc", 60); - testObject.addProperty("effectivePlanSoc", 70); - testObject.addProperty("limitSoc", 80); - testObject.addProperty("vehicleLimitSoc", 90); - testObject.addProperty("vehicleSoc", 90); - JsonArray loadpointArray = new JsonArray(); - loadpointArray.add(testObject); - testState.add("loadpoints", loadpointArray); + handler = spy(createHandler()); + EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); + handler.bridgeHandler = bridgeHandler; + when(bridgeHandler.getCachedEvccState()).thenReturn(exampleResponse); + + heatingObject = verifyObject.getAsJsonArray(JSON_KEY_LOADPOINTS).get(0).getAsJsonObject(); + heatingObject.remove(JSON_KEY_EFFECTIVE_LIMIT_SOC); + heatingObject.remove(JSON_KEY_VEHICLE_SOC); + heatingObject.remove(JSON_KEY_LIMIT_SOC); + heatingObject.remove(JSON_KEY_EFFECTIVE_PLAN_SOC); + heatingObject.remove(JSON_KEY_VEHICLE_LIMIT_SOC); + heatingObject.addProperty("effectiveLimitTemperature", 80); + heatingObject.addProperty("vehicleTemperature", 48.3); + heatingObject.addProperty("limitTemperature", 80); + heatingObject.addProperty("effectivePlanTemperature", 20); + heatingObject.addProperty("vehicleLimitTemperature", 80); } @SuppressWarnings("null") @Test public void testInitializeWithBridgeHandlerWithValidState() { - EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); - handler.bridgeHandler = bridgeHandler; - - when(bridgeHandler.getCachedEvccState()).thenReturn(testState); - handler.initialize(); assertSame(ThingStatus.ONLINE, lastThingStatus); } @@ -117,33 +107,30 @@ public void testInitializeWithBridgeHandlerWithValidState() { @SuppressWarnings("null") @Test public void testPrepareApiResponseForChannelStateUpdateIsInitialized() { - handler.bridgeHandler = mock(EvccBridgeHandler.class); handler.isInitialized = true; - handler.prepareApiResponseForChannelStateUpdate(testState); + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); assertSame(ThingStatus.ONLINE, lastThingStatus); } @SuppressWarnings("null") @Test public void testPrepareApiResponseForChannelStateUpdateIsNotInitialized() { - handler.bridgeHandler = mock(EvccBridgeHandler.class); - - handler.prepareApiResponseForChannelStateUpdate(testState); + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); assertSame(ThingStatus.UNKNOWN, lastThingStatus); } @SuppressWarnings("null") @Test public void testJsonGetsModifiedCorrectly() { - handler.prepareApiResponseForChannelStateUpdate(testState); - assertEquals(verifyObject, testState.getAsJsonArray("loadpoints").get(0)); + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); + assertEquals(heatingObject, exampleResponse.getAsJsonArray("loadpoints").get(0)); } @SuppressWarnings("null") @Test public void testGetStateFromCachedState() { - JsonObject result = handler.getStateFromCachedState(testState); - assertSame(testState.getAsJsonArray("loadpoints").get(0), result); + JsonObject result = handler.getStateFromCachedState(exampleResponse); + assertSame(exampleResponse.getAsJsonArray("loadpoints").get(0), result); } } diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java index 427ad37869161..fa1d53254c3f3 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java @@ -43,9 +43,10 @@ @NonNullByDefault public class EvccLoadpointHandlerTest extends AbstractThingHandlerTestClass { - private final JsonObject testState = new JsonObject(); - private final JsonObject testObject = new JsonObject(); - private final JsonObject verifyObject = new JsonObject(); + private final JsonObject modifiedTestState = exampleResponse.deepCopy(); + private final JsonObject testObject = exampleResponse.getAsJsonArray("loadpoints").get(0).getAsJsonObject(); + private final JsonObject modifiedVerifyObject = verifyObject.deepCopy().getAsJsonArray("loadpoints").get(0) + .getAsJsonObject(); @Override protected EvccLoadpointHandler createHandler() { @@ -80,27 +81,28 @@ public void updateThing(Thing thing) { @BeforeEach public void setup() { - handler = spy(createHandler()); when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "loadpoint")); when(thing.getChannels()).thenReturn(new ArrayList<>()); + handler = spy(createHandler()); - verifyObject.addProperty("chargedEnergy", 50); - verifyObject.addProperty(JSON_KEY_OFFERED_CURRENT, 6); - verifyObject.addProperty(JSON_KEY_CONNECTED, true); - verifyObject.addProperty(JSON_KEY_CHARGING, true); - verifyObject.addProperty(JSON_KEY_PHASES_CONFIGURED, "3"); - verifyObject.addProperty("chargeCurrentL1", 6); - verifyObject.addProperty("chargeCurrentL2", 7); - verifyObject.addProperty("chargeCurrentL3", 8); - verifyObject.addProperty("chargeVoltageL1", 230.0); - verifyObject.addProperty("chargeVoltageL2", 231.0); - verifyObject.addProperty("chargeVoltageL3", 229.0); + modifiedVerifyObject.addProperty("chargedEnergy", 50); + modifiedVerifyObject.addProperty(JSON_KEY_OFFERED_CURRENT, 6); + modifiedVerifyObject.addProperty(JSON_KEY_CONNECTED, true); + modifiedVerifyObject.addProperty(JSON_KEY_PHASES_CONFIGURED, "3"); + modifiedVerifyObject.addProperty("chargeCurrentL1", 6); + modifiedVerifyObject.addProperty("chargeCurrentL2", 7); + modifiedVerifyObject.addProperty("chargeCurrentL3", 8); + modifiedVerifyObject.addProperty("chargeVoltageL1", 230.0); + modifiedVerifyObject.addProperty("chargeVoltageL2", 231.0); + modifiedVerifyObject.addProperty("chargeVoltageL3", 229.0); + modifiedVerifyObject.remove(JSON_KEY_CHARGE_CURRENT); + modifiedVerifyObject.remove(JSON_KEY_VEHICLE_PRESENT); + modifiedVerifyObject.remove(JSON_KEY_PHASES); testObject.addProperty("chargedEnergy", 50); testObject.addProperty(JSON_KEY_CHARGE_CURRENT, 6); testObject.addProperty(JSON_KEY_VEHICLE_PRESENT, true); - testObject.addProperty(JSON_KEY_ENABLED, true); testObject.addProperty(JSON_KEY_PHASES, "3"); JsonArray currents = new JsonArray(); currents.add(6); @@ -112,10 +114,9 @@ public void setup() { voltages.add(231.0); voltages.add(229.0); testObject.add(JSON_KEY_CHARGE_VOLTAGES, voltages); - JsonArray loadpointArray = new JsonArray(); - loadpointArray.add(testObject); - testState.add("loadpoints", loadpointArray); - testState.addProperty("version", "0.207.0"); + JsonArray loadpointArray = exampleResponse.getAsJsonArray("loadpoints"); + loadpointArray.set(0, testObject); + modifiedTestState.add("loadpoints", loadpointArray); } @SuppressWarnings("null") @@ -123,7 +124,7 @@ public void setup() { public void testInitializeWithBridgeHandlerWithValidState() { EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); handler.bridgeHandler = bridgeHandler; - when(bridgeHandler.getCachedEvccState()).thenReturn(testState); + when(bridgeHandler.getCachedEvccState()).thenReturn(exampleResponse); handler.initialize(); assertSame(ThingStatus.ONLINE, lastThingStatus); @@ -135,7 +136,7 @@ public void testPrepareApiResponseForChannelStateUpdateIsInitialized() { handler.bridgeHandler = mock(EvccBridgeHandler.class); handler.isInitialized = true; - handler.prepareApiResponseForChannelStateUpdate(testState); + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); assertSame(ThingStatus.ONLINE, lastThingStatus); } @@ -144,21 +145,21 @@ public void testPrepareApiResponseForChannelStateUpdateIsInitialized() { public void testPrepareApiResponseForChannelStateUpdateIsNotInitialized() { handler.bridgeHandler = mock(EvccBridgeHandler.class); - handler.prepareApiResponseForChannelStateUpdate(testState); + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); assertSame(ThingStatus.UNKNOWN, lastThingStatus); } @SuppressWarnings("null") @Test public void testJsonGetsModifiedCorrectly() { - handler.prepareApiResponseForChannelStateUpdate(testState); - assertEquals(verifyObject, testState.getAsJsonArray("loadpoints").get(0)); + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); + assertEquals(modifiedVerifyObject, modifiedTestState.getAsJsonArray("loadpoints").get(0)); } @SuppressWarnings("null") @Test public void testGetStateFromCachedState() { - JsonObject result = handler.getStateFromCachedState(testState); - assertSame(testState.getAsJsonArray("loadpoints").get(0), result); + JsonObject result = handler.getStateFromCachedState(exampleResponse); + assertSame(exampleResponse.getAsJsonArray("loadpoints").get(0), result); } } diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java index fe60e920a0045..c2b7327be6778 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java @@ -78,10 +78,10 @@ public void updateThing(Thing thing) { @BeforeEach public void setup() { - handler = spy(createHandler()); when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "pv")); when(thing.getChannels()).thenReturn(new ArrayList<>()); + handler = spy(createHandler()); verifyObject.addProperty("power", 2000); diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandlerTest.java index 88ee8d880776d..fa31129be550f 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandlerTest.java @@ -43,9 +43,8 @@ @NonNullByDefault public class EvccSiteHandlerTest extends AbstractThingHandlerTestClass { - private final JsonObject testState = new JsonObject(); - private final JsonObject verifyObject = new JsonObject(); private final JsonObject gridConfigured = new JsonObject(); + private final JsonObject modifiedVerifyObject = verifyObject.deepCopy(); @Override protected EvccSiteHandler createHandler() { @@ -80,20 +79,21 @@ public void updateThing(Thing thing) { @BeforeEach public void setup() { - handler = spy(createHandler()); when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "pv")); when(thing.getChannels()).thenReturn(new ArrayList<>()); + handler = spy(createHandler()); - verifyObject.addProperty("version", "0.207.0"); - verifyObject.addProperty("gridPower", 2000); - verifyObject.addProperty("gridEnergy", 10000); - verifyObject.addProperty("gridCurrentL1", 6); - verifyObject.addProperty("gridCurrentL2", 7); - verifyObject.addProperty("gridCurrentL3", 8); - verifyObject.addProperty("gridVoltageL1", 230.0); - verifyObject.addProperty("gridVoltageL2", 231.0); - verifyObject.addProperty("gridVoltageL3", 229.0); + modifiedVerifyObject.addProperty("gridPower", 2000); + modifiedVerifyObject.addProperty("gridEnergy", 10000); + modifiedVerifyObject.addProperty("gridCurrentL1", 6); + modifiedVerifyObject.addProperty("gridCurrentL2", 7); + modifiedVerifyObject.addProperty("gridCurrentL3", 8); + modifiedVerifyObject.addProperty("gridVoltageL1", 230.0); + modifiedVerifyObject.addProperty("gridVoltageL2", 231.0); + modifiedVerifyObject.addProperty("gridVoltageL3", 229.0); + modifiedVerifyObject.remove("gridConfigured"); + modifiedVerifyObject.remove("grid"); gridConfigured.addProperty("power", 2000); gridConfigured.addProperty("energy", 10000); @@ -107,7 +107,6 @@ public void setup() { voltages.add(231.0); voltages.add(229.0); gridConfigured.add("voltages", voltages); - testState.addProperty("version", "0.207.0"); } @SuppressWarnings("null") @@ -115,7 +114,7 @@ public void setup() { public void testInitializeWithBridgeHandlerWithValidState() { EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); handler.bridgeHandler = bridgeHandler; - when(bridgeHandler.getCachedEvccState()).thenReturn(testState); + when(bridgeHandler.getCachedEvccState()).thenReturn(exampleResponse); handler.initialize(); assertSame(ThingStatus.ONLINE, lastThingStatus); @@ -130,7 +129,7 @@ public void handlerIsInitialized() { handler.bridgeHandler = mock(EvccBridgeHandler.class); handler.isInitialized = true; - handler.prepareApiResponseForChannelStateUpdate(testState); + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); assertSame(ThingStatus.ONLINE, lastThingStatus); } @@ -138,7 +137,7 @@ public void handlerIsInitialized() { public void handlerIsNotInitialized() { handler.bridgeHandler = mock(EvccBridgeHandler.class); - handler.prepareApiResponseForChannelStateUpdate(testState); + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); assertSame(ThingStatus.UNKNOWN, lastThingStatus); } @@ -146,17 +145,17 @@ public void handlerIsNotInitialized() { public void stateContainsGridConfigured() { handler.bridgeHandler = mock(EvccBridgeHandler.class); - testState.addProperty("gridConfigured", true); - testState.add("grid", gridConfigured); - handler.prepareApiResponseForChannelStateUpdate(testState); - assertEquals(verifyObject, testState); + exampleResponse.addProperty("gridConfigured", true); + exampleResponse.add("grid", gridConfigured); + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); + assertEquals(modifiedVerifyObject, exampleResponse); } } @SuppressWarnings("null") @Test public void testGetStateFromCachedState() { - JsonObject result = handler.getStateFromCachedState(testState); - assertSame(testState, result); + JsonObject result = handler.getStateFromCachedState(exampleResponse); + assertSame(exampleResponse, result); } } diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandlerTest.java new file mode 100644 index 0000000000000..989a761faf576 --- /dev/null +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandlerTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.evcc.internal.handler; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; + +import java.util.ArrayList; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.types.State; + +/** + * The {@link EvccStatisticsHandlerTest} is responsible for testing the EvccSiteHandler implementation + * + * @author Marcel Goerentz - Initial contribution + */ +@NonNullByDefault +public class EvccStatisticsHandlerTest extends AbstractThingHandlerTestClass { + + private boolean updateStateCalled = false; + private int updateStateCounter = 0; + + @Override + protected EvccStatisticsHandler createHandler() { + return new EvccStatisticsHandler(thing, channelTypeRegistry) { + + @Override + protected void updateStatus(ThingStatus status, ThingStatusDetail detail) { + lastThingStatus = status; + lastThingStatusDetail = detail; + } + + @Override + protected void updateStatus(ThingStatus status) { + lastThingStatus = status; + } + + @Override + public void logUnknownChannelXmlAsync(String key, String itemType) { + } + + @Nullable + @Override + protected Bridge getBridge() { + return null; + } + + @Override + public void updateState(ChannelUID uid, State state) { + updateStateCalled = true; + updateStateCounter++; + } + }; + } + + @SuppressWarnings("null") + @BeforeEach + public void setup() { + when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); + when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "statistics")); + when(thing.getChannels()).thenReturn(new ArrayList<>()); + handler = spy(createHandler()); + EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); + handler.bridgeHandler = bridgeHandler; + when(bridgeHandler.getCachedEvccState()).thenReturn(exampleResponse); + } + + @SuppressWarnings("null") + @Nested + public class TestPrepareApiResponseForChannelStateUpdate { + + @Test + public void handlerIsInitialized() { + handler.isInitialized = true; + + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); + assertTrue(updateStateCalled); + assertEquals(16, updateStateCounter); + assertSame(ThingStatus.ONLINE, lastThingStatus); + } + + @Test + public void handlerIsNotInitialized() { + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); + assertSame(ThingStatus.OFFLINE, lastThingStatus); + } + } +} diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandlerTest.java new file mode 100644 index 0000000000000..3c36a6acfcc77 --- /dev/null +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandlerTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.evcc.internal.handler; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingUID; + +/** + * The {@link EvccVehicleHandlerTest} is responsible for testing the EvccSiteHandler implementation + * + * @author Marcel Goerentz - Initial contribution + */ +@NonNullByDefault +public class EvccVehicleHandlerTest extends AbstractThingHandlerTestClass { + + @Override + protected EvccVehicleHandler createHandler() { + return new EvccVehicleHandler(thing, channelTypeRegistry) { + + @Override + protected void updateStatus(ThingStatus status, ThingStatusDetail detail) { + lastThingStatus = status; + lastThingStatusDetail = detail; + } + + @Override + protected void updateStatus(ThingStatus status) { + lastThingStatus = status; + } + + @Override + public void logUnknownChannelXmlAsync(String key, String itemType) { + } + + @Nullable + @Override + protected Bridge getBridge() { + return null; + } + + @Override + public void updateThing(Thing thing) { + } + }; + } + + @SuppressWarnings("null") + @BeforeEach + public void setup() { + when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); + when(thing.getProperties()).thenReturn(Map.of("id", "vehicle_1", "type", "vehicle")); + when(thing.getChannels()).thenReturn(new ArrayList<>()); + handler = spy(createHandler()); + EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); + handler.bridgeHandler = bridgeHandler; + when(bridgeHandler.getCachedEvccState()).thenReturn(exampleResponse); + } + + @SuppressWarnings("null") + @Test + public void testInitializeWithBridgeHandlerWithValidState() { + handler.initialize(); + assertSame(ThingStatus.ONLINE, lastThingStatus); + } +} diff --git a/bundles/org.openhab.binding.evcc/src/test/resources/responses/example_response.json b/bundles/org.openhab.binding.evcc/src/test/resources/responses/example_response.json new file mode 100644 index 0000000000000..ac8fbc60afa06 --- /dev/null +++ b/bundles/org.openhab.binding.evcc/src/test/resources/responses/example_response.json @@ -0,0 +1,1243 @@ +{ + "authProviders": { + }, + "aux": [ + ], + "battery": [ + { + "power": 188.78599363476042, + "capacity": 13.4, + "soc": 31, + "controllable": true + } + ], + "batteryCapacity": 13.4, + "batteryDischargeControl": true, + "batteryEnergy": 0, + "batteryGridChargeActive": true, + "batteryGridChargeLimit": 0.25, + "batteryMode": "charge", + "batteryModeExternal": "unknown", + "batteryPower": 188.786, + "batterySoc": 31, + "bufferSoc": 75, + "bufferStartSoc": 0, + "currency": "EUR", + "demoMode": true, + "eebus": false, + "ext": [ + ], + "forecast": { + "co2": [ + { + "start": "2025-09-08T00:00:00+02:00", + "end": "2025-09-08T01:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-08T01:00:00+02:00", + "end": "2025-09-08T02:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-08T02:00:00+02:00", + "end": "2025-09-08T03:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-08T03:00:00+02:00", + "end": "2025-09-08T04:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-08T04:00:00+02:00", + "end": "2025-09-08T05:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-08T05:00:00+02:00", + "end": "2025-09-08T06:00:00+02:00", + "value": 462 + }, + { + "start": "2025-09-08T06:00:00+02:00", + "end": "2025-09-08T07:00:00+02:00", + "value": 462 + }, + { + "start": "2025-09-08T07:00:00+02:00", + "end": "2025-09-08T08:00:00+02:00", + "value": 462 + }, + { + "start": "2025-09-08T08:00:00+02:00", + "end": "2025-09-08T09:00:00+02:00", + "value": 462 + }, + { + "start": "2025-09-08T09:00:00+02:00", + "end": "2025-09-08T10:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-08T10:00:00+02:00", + "end": "2025-09-08T11:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-08T11:00:00+02:00", + "end": "2025-09-08T12:00:00+02:00", + "value": 252 + }, + { + "start": "2025-09-08T12:00:00+02:00", + "end": "2025-09-08T13:00:00+02:00", + "value": 252 + }, + { + "start": "2025-09-08T13:00:00+02:00", + "end": "2025-09-08T14:00:00+02:00", + "value": 252 + }, + { + "start": "2025-09-08T14:00:00+02:00", + "end": "2025-09-08T15:00:00+02:00", + "value": 252 + }, + { + "start": "2025-09-08T15:00:00+02:00", + "end": "2025-09-08T16:00:00+02:00", + "value": 322 + }, + { + "start": "2025-09-08T16:00:00+02:00", + "end": "2025-09-08T17:00:00+02:00", + "value": 322 + }, + { + "start": "2025-09-08T17:00:00+02:00", + "end": "2025-09-08T18:00:00+02:00", + "value": 434 + }, + { + "start": "2025-09-08T18:00:00+02:00", + "end": "2025-09-08T19:00:00+02:00", + "value": 434 + }, + { + "start": "2025-09-08T19:00:00+02:00", + "end": "2025-09-08T20:00:00+02:00", + "value": 434 + }, + { + "start": "2025-09-08T20:00:00+02:00", + "end": "2025-09-08T21:00:00+02:00", + "value": 434 + }, + { + "start": "2025-09-08T21:00:00+02:00", + "end": "2025-09-08T22:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-08T22:00:00+02:00", + "end": "2025-09-08T23:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-08T23:00:00+02:00", + "end": "2025-09-09T00:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-09T00:00:00+02:00", + "end": "2025-09-09T01:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-09T01:00:00+02:00", + "end": "2025-09-09T02:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-09T02:00:00+02:00", + "end": "2025-09-09T03:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-09T03:00:00+02:00", + "end": "2025-09-09T04:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-09T04:00:00+02:00", + "end": "2025-09-09T05:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-09T05:00:00+02:00", + "end": "2025-09-09T06:00:00+02:00", + "value": 462 + }, + { + "start": "2025-09-09T06:00:00+02:00", + "end": "2025-09-09T07:00:00+02:00", + "value": 462 + }, + { + "start": "2025-09-09T07:00:00+02:00", + "end": "2025-09-09T08:00:00+02:00", + "value": 462 + }, + { + "start": "2025-09-09T08:00:00+02:00", + "end": "2025-09-09T09:00:00+02:00", + "value": 462 + }, + { + "start": "2025-09-09T09:00:00+02:00", + "end": "2025-09-09T10:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-09T10:00:00+02:00", + "end": "2025-09-09T11:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-09T11:00:00+02:00", + "end": "2025-09-09T12:00:00+02:00", + "value": 252 + }, + { + "start": "2025-09-09T12:00:00+02:00", + "end": "2025-09-09T13:00:00+02:00", + "value": 252 + }, + { + "start": "2025-09-09T13:00:00+02:00", + "end": "2025-09-09T14:00:00+02:00", + "value": 252 + }, + { + "start": "2025-09-09T14:00:00+02:00", + "end": "2025-09-09T15:00:00+02:00", + "value": 252 + }, + { + "start": "2025-09-09T15:00:00+02:00", + "end": "2025-09-09T16:00:00+02:00", + "value": 322 + }, + { + "start": "2025-09-09T16:00:00+02:00", + "end": "2025-09-09T17:00:00+02:00", + "value": 322 + }, + { + "start": "2025-09-09T17:00:00+02:00", + "end": "2025-09-09T18:00:00+02:00", + "value": 434 + }, + { + "start": "2025-09-09T18:00:00+02:00", + "end": "2025-09-09T19:00:00+02:00", + "value": 434 + }, + { + "start": "2025-09-09T19:00:00+02:00", + "end": "2025-09-09T20:00:00+02:00", + "value": 434 + }, + { + "start": "2025-09-09T20:00:00+02:00", + "end": "2025-09-09T21:00:00+02:00", + "value": 434 + }, + { + "start": "2025-09-09T21:00:00+02:00", + "end": "2025-09-09T22:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-09T22:00:00+02:00", + "end": "2025-09-09T23:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-09T23:00:00+02:00", + "end": "2025-09-10T00:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-10T00:00:00+02:00", + "end": "2025-09-10T01:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-10T01:00:00+02:00", + "end": "2025-09-10T02:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-10T02:00:00+02:00", + "end": "2025-09-10T03:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-10T03:00:00+02:00", + "end": "2025-09-10T04:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-10T04:00:00+02:00", + "end": "2025-09-10T05:00:00+02:00", + "value": 378 + }, + { + "start": "2025-09-10T05:00:00+02:00", + "end": "2025-09-10T06:00:00+02:00", + "value": 462 + }, + { + "start": "2025-09-10T06:00:00+02:00", + "end": "2025-09-10T07:00:00+02:00", + "value": 462 + }, + { + "start": "2025-09-10T07:00:00+02:00", + "end": "2025-09-10T08:00:00+02:00", + "value": 462 + }, + { + "start": "2025-09-10T08:00:00+02:00", + "end": "2025-09-10T09:00:00+02:00", + "value": 462 + }, + { + "start": "2025-09-10T09:00:00+02:00", + "end": "2025-09-10T10:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-10T10:00:00+02:00", + "end": "2025-09-10T11:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-10T11:00:00+02:00", + "end": "2025-09-10T12:00:00+02:00", + "value": 252 + }, + { + "start": "2025-09-10T12:00:00+02:00", + "end": "2025-09-10T13:00:00+02:00", + "value": 252 + }, + { + "start": "2025-09-10T13:00:00+02:00", + "end": "2025-09-10T14:00:00+02:00", + "value": 252 + }, + { + "start": "2025-09-10T14:00:00+02:00", + "end": "2025-09-10T15:00:00+02:00", + "value": 252 + }, + { + "start": "2025-09-10T15:00:00+02:00", + "end": "2025-09-10T16:00:00+02:00", + "value": 322 + }, + { + "start": "2025-09-10T16:00:00+02:00", + "end": "2025-09-10T17:00:00+02:00", + "value": 322 + }, + { + "start": "2025-09-10T17:00:00+02:00", + "end": "2025-09-10T18:00:00+02:00", + "value": 434 + }, + { + "start": "2025-09-10T18:00:00+02:00", + "end": "2025-09-10T19:00:00+02:00", + "value": 434 + }, + { + "start": "2025-09-10T19:00:00+02:00", + "end": "2025-09-10T20:00:00+02:00", + "value": 434 + }, + { + "start": "2025-09-10T20:00:00+02:00", + "end": "2025-09-10T21:00:00+02:00", + "value": 434 + }, + { + "start": "2025-09-10T21:00:00+02:00", + "end": "2025-09-10T22:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-10T22:00:00+02:00", + "end": "2025-09-10T23:00:00+02:00", + "value": 392 + }, + { + "start": "2025-09-10T23:00:00+02:00", + "end": "2025-09-11T00:00:00+02:00", + "value": 392 + } + ], + "grid": [ + { + "start": "2025-09-08T00:00:00+02:00", + "end": "2025-09-08T01:00:00+02:00", + "value": 0.22965999999999998 + }, + { + "start": "2025-09-08T01:00:00+02:00", + "end": "2025-09-08T02:00:00+02:00", + "value": 0.23574 + }, + { + "start": "2025-09-08T02:00:00+02:00", + "end": "2025-09-08T03:00:00+02:00", + "value": 0.2347 + }, + { + "start": "2025-09-08T03:00:00+02:00", + "end": "2025-09-08T04:00:00+02:00", + "value": 0.23226999999999998 + }, + { + "start": "2025-09-08T04:00:00+02:00", + "end": "2025-09-08T05:00:00+02:00", + "value": 0.23379 + }, + { + "start": "2025-09-08T05:00:00+02:00", + "end": "2025-09-08T06:00:00+02:00", + "value": 0.24559999999999998 + }, + { + "start": "2025-09-08T06:00:00+02:00", + "end": "2025-09-08T07:00:00+02:00", + "value": 0.2599 + }, + { + "start": "2025-09-08T07:00:00+02:00", + "end": "2025-09-08T08:00:00+02:00", + "value": 0.29813999999999996 + }, + { + "start": "2025-09-08T08:00:00+02:00", + "end": "2025-09-08T09:00:00+02:00", + "value": 0.27553 + }, + { + "start": "2025-09-08T09:00:00+02:00", + "end": "2025-09-08T10:00:00+02:00", + "value": 0.25205 + }, + { + "start": "2025-09-08T10:00:00+02:00", + "end": "2025-09-08T11:00:00+02:00", + "value": 0.23582999999999998 + }, + { + "start": "2025-09-08T11:00:00+02:00", + "end": "2025-09-08T12:00:00+02:00", + "value": 0.22614 + }, + { + "start": "2025-09-08T12:00:00+02:00", + "end": "2025-09-08T13:00:00+02:00", + "value": 0.21534999999999999 + }, + { + "start": "2025-09-08T13:00:00+02:00", + "end": "2025-09-08T14:00:00+02:00", + "value": 0.21084 + }, + { + "start": "2025-09-08T14:00:00+02:00", + "end": "2025-09-08T15:00:00+02:00", + "value": 0.21965 + }, + { + "start": "2025-09-08T15:00:00+02:00", + "end": "2025-09-08T16:00:00+02:00", + "value": 0.22846 + }, + { + "start": "2025-09-08T16:00:00+02:00", + "end": "2025-09-08T17:00:00+02:00", + "value": 0.24972 + }, + { + "start": "2025-09-08T17:00:00+02:00", + "end": "2025-09-08T18:00:00+02:00", + "value": 0.27427999999999997 + }, + { + "start": "2025-09-08T18:00:00+02:00", + "end": "2025-09-08T19:00:00+02:00", + "value": 0.34661 + }, + { + "start": "2025-09-08T19:00:00+02:00", + "end": "2025-09-08T20:00:00+02:00", + "value": 0.56366 + }, + { + "start": "2025-09-08T20:00:00+02:00", + "end": "2025-09-08T21:00:00+02:00", + "value": 0.45851999999999993 + }, + { + "start": "2025-09-08T21:00:00+02:00", + "end": "2025-09-08T22:00:00+02:00", + "value": 0.302 + }, + { + "start": "2025-09-08T22:00:00+02:00", + "end": "2025-09-08T23:00:00+02:00", + "value": 0.27393 + }, + { + "start": "2025-09-08T23:00:00+02:00", + "end": "2025-09-09T00:00:00+02:00", + "value": 0.26312 + } + ], + "planner": [ + { + "start": "2025-09-08T00:00:00+02:00", + "end": "2025-09-08T01:00:00+02:00", + "value": 0.22965999999999998 + }, + { + "start": "2025-09-08T01:00:00+02:00", + "end": "2025-09-08T02:00:00+02:00", + "value": 0.23574 + }, + { + "start": "2025-09-08T02:00:00+02:00", + "end": "2025-09-08T03:00:00+02:00", + "value": 0.2347 + }, + { + "start": "2025-09-08T03:00:00+02:00", + "end": "2025-09-08T04:00:00+02:00", + "value": 0.23226999999999998 + }, + { + "start": "2025-09-08T04:00:00+02:00", + "end": "2025-09-08T05:00:00+02:00", + "value": 0.23379 + }, + { + "start": "2025-09-08T05:00:00+02:00", + "end": "2025-09-08T06:00:00+02:00", + "value": 0.24559999999999998 + }, + { + "start": "2025-09-08T06:00:00+02:00", + "end": "2025-09-08T07:00:00+02:00", + "value": 0.2599 + }, + { + "start": "2025-09-08T07:00:00+02:00", + "end": "2025-09-08T08:00:00+02:00", + "value": 0.29813999999999996 + }, + { + "start": "2025-09-08T08:00:00+02:00", + "end": "2025-09-08T09:00:00+02:00", + "value": 0.27553 + }, + { + "start": "2025-09-08T09:00:00+02:00", + "end": "2025-09-08T10:00:00+02:00", + "value": 0.25205 + }, + { + "start": "2025-09-08T10:00:00+02:00", + "end": "2025-09-08T11:00:00+02:00", + "value": 0.23582999999999998 + }, + { + "start": "2025-09-08T11:00:00+02:00", + "end": "2025-09-08T12:00:00+02:00", + "value": 0.22614 + }, + { + "start": "2025-09-08T12:00:00+02:00", + "end": "2025-09-08T13:00:00+02:00", + "value": 0.21534999999999999 + }, + { + "start": "2025-09-08T13:00:00+02:00", + "end": "2025-09-08T14:00:00+02:00", + "value": 0.21084 + }, + { + "start": "2025-09-08T14:00:00+02:00", + "end": "2025-09-08T15:00:00+02:00", + "value": 0.21965 + }, + { + "start": "2025-09-08T15:00:00+02:00", + "end": "2025-09-08T16:00:00+02:00", + "value": 0.22846 + }, + { + "start": "2025-09-08T16:00:00+02:00", + "end": "2025-09-08T17:00:00+02:00", + "value": 0.24972 + }, + { + "start": "2025-09-08T17:00:00+02:00", + "end": "2025-09-08T18:00:00+02:00", + "value": 0.27427999999999997 + }, + { + "start": "2025-09-08T18:00:00+02:00", + "end": "2025-09-08T19:00:00+02:00", + "value": 0.34661 + }, + { + "start": "2025-09-08T19:00:00+02:00", + "end": "2025-09-08T20:00:00+02:00", + "value": 0.56366 + }, + { + "start": "2025-09-08T20:00:00+02:00", + "end": "2025-09-08T21:00:00+02:00", + "value": 0.45851999999999993 + }, + { + "start": "2025-09-08T21:00:00+02:00", + "end": "2025-09-08T22:00:00+02:00", + "value": 0.302 + }, + { + "start": "2025-09-08T22:00:00+02:00", + "end": "2025-09-08T23:00:00+02:00", + "value": 0.27393 + }, + { + "start": "2025-09-08T23:00:00+02:00", + "end": "2025-09-09T00:00:00+02:00", + "value": 0.26312 + } + ], + "solar": { + "scale": 1.7142189483634065, + "today": { + "energy": 46007.892876756465, + "complete": true + }, + "tomorrow": { + "energy": 112728, + "complete": true + }, + "dayAfterTomorrow": { + "energy": 112728, + "complete": false + }, + "timeseries": [ + { + "ts": "2025-09-08T00:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-08T01:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-08T02:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-08T03:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-08T04:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-08T05:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-08T06:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-08T07:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-08T08:00:00+02:00", + "val": 5124 + }, + { + "ts": "2025-09-08T09:00:00+02:00", + "val": 9223 + }, + { + "ts": "2025-09-08T10:00:00+02:00", + "val": 12298 + }, + { + "ts": "2025-09-08T11:00:00+02:00", + "val": 14347 + }, + { + "ts": "2025-09-08T12:00:00+02:00", + "val": 15372 + }, + { + "ts": "2025-09-08T13:00:00+02:00", + "val": 15372 + }, + { + "ts": "2025-09-08T14:00:00+02:00", + "val": 14347 + }, + { + "ts": "2025-09-08T15:00:00+02:00", + "val": 12298 + }, + { + "ts": "2025-09-08T16:00:00+02:00", + "val": 9223 + }, + { + "ts": "2025-09-08T17:00:00+02:00", + "val": 5124 + }, + { + "ts": "2025-09-08T18:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-08T19:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-08T20:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-08T21:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-08T22:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-08T23:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T00:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T01:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T02:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T03:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T04:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T05:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T06:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T07:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T08:00:00+02:00", + "val": 5124 + }, + { + "ts": "2025-09-09T09:00:00+02:00", + "val": 9223 + }, + { + "ts": "2025-09-09T10:00:00+02:00", + "val": 12298 + }, + { + "ts": "2025-09-09T11:00:00+02:00", + "val": 14347 + }, + { + "ts": "2025-09-09T12:00:00+02:00", + "val": 15372 + }, + { + "ts": "2025-09-09T13:00:00+02:00", + "val": 15372 + }, + { + "ts": "2025-09-09T14:00:00+02:00", + "val": 14347 + }, + { + "ts": "2025-09-09T15:00:00+02:00", + "val": 12298 + }, + { + "ts": "2025-09-09T16:00:00+02:00", + "val": 9223 + }, + { + "ts": "2025-09-09T17:00:00+02:00", + "val": 5124 + }, + { + "ts": "2025-09-09T18:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T19:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T20:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T21:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T22:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-09T23:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T00:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T01:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T02:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T03:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T04:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T05:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T06:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T07:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T08:00:00+02:00", + "val": 5124 + }, + { + "ts": "2025-09-10T09:00:00+02:00", + "val": 9223 + }, + { + "ts": "2025-09-10T10:00:00+02:00", + "val": 12298 + }, + { + "ts": "2025-09-10T11:00:00+02:00", + "val": 14347 + }, + { + "ts": "2025-09-10T12:00:00+02:00", + "val": 15372 + }, + { + "ts": "2025-09-10T13:00:00+02:00", + "val": 15372 + }, + { + "ts": "2025-09-10T14:00:00+02:00", + "val": 14347 + }, + { + "ts": "2025-09-10T15:00:00+02:00", + "val": 12298 + }, + { + "ts": "2025-09-10T16:00:00+02:00", + "val": 9223 + }, + { + "ts": "2025-09-10T17:00:00+02:00", + "val": 5124 + }, + { + "ts": "2025-09-10T18:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T19:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T20:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T21:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T22:00:00+02:00", + "val": 0 + }, + { + "ts": "2025-09-10T23:00:00+02:00", + "val": 0 + } + ] + } + }, + "greenShareHome": 1, + "greenShareLoadpoints": 0.739, + "grid": { + "power": 2875.972497154853 + }, + "gridConfigured": true, + "hems": { + }, + "homePower": 500, + "influx": { + "url": "", + "database": "", + "token": "", + "org": "", + "user": "", + "password": "", + "insecure": false + }, + "interval": 3, + "loadpoints": [ + { + "batteryBoost": false, + "chargeDuration": 743853, + "chargePower": 0, + "chargeRemainingDuration": 0, + "chargeRemainingEnergy": 2364.153, + "chargedEnergy": 1182321.351, + "chargerFeatureHeating": false, + "chargerFeatureIntegratedDevice": false, + "chargerIcon": "", + "chargerPhases1p3p": false, + "chargerSinglePhase": false, + "chargerStatusReason": "unknown", + "charging": false, + "connected": true, + "connectedDuration": 0, + "disableDelay": 180, + "disableThreshold": 0, + "effectiveLimitSoc": 80, + "effectiveMaxCurrent": 15, + "effectiveMinCurrent": 6, + "effectivePlanId": 0, + "effectivePlanSoc": 20, + "effectivePlanTime": null, + "effectivePriority": 0, + "enableDelay": 60, + "enableThreshold": 0, + "enabled": false, + "limitEnergy": 0, + "limitSoc": 80, + "maxCurrent": 15, + "minCurrent": 6, + "mode": "pv", + "offeredCurrent": 0, + "phaseAction": "inactive", + "phaseRemaining": 0, + "phasesActive": 1, + "phasesConfigured": 1, + "planActive": false, + "planEnergy": 0, + "planOverrun": 0, + "planPrecondition": 0, + "planProjectedEnd": null, + "planProjectedStart": null, + "planTime": null, + "priority": 0, + "pvAction": "inactive", + "pvRemaining": 0, + "sessionCo2PerKWh": 80.991, + "sessionEnergy": 1182321.351, + "sessionPrice": 134.88, + "sessionPricePerKWh": 0.114, + "sessionSolarPercentage": 77.772, + "smartCostActive": false, + "smartCostLimit": 0, + "smartCostNextStart": null, + "smartFeedInPriorityActive": false, + "smartFeedInPriorityLimit": null, + "smartFeedInPriorityNextStart": null, + "title": "Carport", + "vehicleClimaterActive": null, + "vehicleDetectionActive": false, + "vehicleLimitSoc": 80, + "vehicleName": "vehicle_1", + "vehicleOdometer": 0, + "vehicleRange": 141, + "vehicleSoc": 48.3, + "vehicleTitle": "blauer e-Golf", + "vehicleWelcomeActive": false + }, + { + "batteryBoost": false, + "chargeDuration": 712180, + "chargePower": 11040, + "chargeRemainingDuration": 0, + "chargeRemainingEnergy": 0, + "chargedEnergy": 1257162.744, + "chargerFeatureHeating": false, + "chargerFeatureIntegratedDevice": false, + "chargerIcon": "", + "chargerPhases1p3p": true, + "chargerSinglePhase": false, + "chargerStatusReason": "unknown", + "charging": true, + "connected": true, + "connectedDuration": 0, + "disableDelay": 180, + "disableThreshold": 0, + "effectiveLimitSoc": 85, + "effectiveMaxCurrent": 16, + "effectiveMinCurrent": 6, + "effectivePlanId": 0, + "effectivePlanSoc": 0, + "effectivePlanTime": null, + "effectivePriority": 0, + "enableDelay": 60, + "enableThreshold": 0, + "enabled": true, + "limitEnergy": 0, + "limitSoc": 85, + "maxCurrent": 16, + "minCurrent": 6, + "mode": "now", + "offeredCurrent": 16, + "phaseAction": "inactive", + "phaseRemaining": 0, + "phasesActive": 3, + "phasesConfigured": 0, + "planActive": false, + "planEnergy": 0, + "planOverrun": 0, + "planPrecondition": 0, + "planProjectedEnd": null, + "planProjectedStart": null, + "planTime": null, + "priority": 0, + "pvAction": "inactive", + "pvRemaining": 0, + "sessionCo2PerKWh": 71.313, + "sessionEnergy": 1257162.744, + "sessionPrice": 136.41, + "sessionPricePerKWh": 0.109, + "sessionSolarPercentage": 80.384, + "smartCostActive": false, + "smartCostLimit": 0.12, + "smartCostNextStart": null, + "smartFeedInPriorityActive": false, + "smartFeedInPriorityLimit": null, + "smartFeedInPriorityNextStart": null, + "title": "Garage", + "vehicleClimaterActive": null, + "vehicleDetectionActive": false, + "vehicleLimitSoc": 0, + "vehicleName": "vehicle_5", + "vehicleOdometer": 0, + "vehicleRange": 0, + "vehicleSoc": 0, + "vehicleTitle": "Wärmepumpe", + "vehicleWelcomeActive": false + } + ], + "messaging": false, + "modbusproxy": null, + "mqtt": { + "broker": "", + "user": "", + "password": "", + "clientID": "", + "insecure": false, + "caCert": "", + "clientCert": "", + "clientKey": "", + "topic": "evcc" + }, + "network": { + "schema": "http", + "host": "evcc.local", + "port": 7070 + }, + "prioritySoc": 50, + "pv": [ + { + "power": 8475.2415092103874 + } + ], + "pvEnergy": 0, + "pvPower": 8475.242, + "residualPower": 1, + "siteTitle": "Demo Mode", + "smartCostAvailable": true, + "smartCostType": "priceforecast", + "smartFeedInPriorityAvailable": false, + "sponsor": { + "name": "", + "expiresAt": "0001-01-01T00:00:00Z", + "fromYaml": true + }, + "startup": true, + "statistics": { + "30d": { + "avgCo2": 76.040614873472336, + "avgPrice": 0.11118445465324438, + "chargedKWh": 2428.032521160711, + "solarPercentage": 79.147220316136114 + }, + "365d": { + "avgCo2": 76.040614873472336, + "avgPrice": 0.11118445465324438, + "chargedKWh": 2428.032521160711, + "solarPercentage": 79.147220316136114 + }, + "thisYear": { + "avgCo2": 76.040614873472336, + "avgPrice": 0.11118445465324438, + "chargedKWh": 2428.032521160711, + "solarPercentage": 79.147220316136114 + }, + "total": { + "avgCo2": 76.040614873472336, + "avgPrice": 0.11118445465324438, + "chargedKWh": 2428.032521160711, + "solarPercentage": 79.147220316136114 + } + }, + "tariffCo2": 252, + "tariffCo2Home": 0, + "tariffCo2Loadpoints": 65.647, + "tariffFeedIn": 0.08, + "tariffGrid": 0.211, + "tariffPriceHome": 0.08, + "tariffPriceLoadpoints": 0.114, + "tariffSolar": 15372, + "telemetry": false, + "vehicles": { + "vehicle_1": { + "title": "blauer e-Golf", + "capacity": 44, + "minSoc": 15, + "limitSoc": 80, + "repeatingPlans": [ + { + "weekdays": [ + 1, + 2, + 3, + 4, + 5 + ], + "time": "07:00", + "tz": "Europe/Amsterdam", + "soc": 80, + "precondition": 0, + "active": false + } + ] + }, + "vehicle_2": { + "title": "weißes Model 3", + "capacity": 80, + "minSoc": 40, + "limitSoc": 80, + "plan": { + "soc": 80, + "precondition": 3600, + "time": "2025-09-08T06:00:00Z" + }, + "repeatingPlans": [ + ] + }, + "vehicle_3": { + "title": "grüner Honda e", + "icon": "car", + "capacity": 8, + "features": [ + "Offline" + ], + "repeatingPlans": [ + ] + }, + "vehicle_4": { + "title": "schwarzes VanMoof", + "icon": "bike", + "capacity": 0.46, + "features": [ + "Offline" + ], + "repeatingPlans": [ + ] + }, + "vehicle_5": { + "title": "Wärmepumpe", + "icon": "waterheater", + "features": [ + "Offline" + ], + "repeatingPlans": [ + ] + } + }, + "version": "0.207.5" +} \ No newline at end of file From cd89e4c0c69b85efdbf0482e47228126ba228c88 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:30:38 +0200 Subject: [PATCH 14/29] Update bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml Using uppercase for unit hint Co-authored-by: lsiepel Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../src/main/resources/OH-INF/thing/loadpoint.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml index 4d55c804f5e6d..0a88bbc1a2d24 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml @@ -489,7 +489,7 @@ - Number:ElectricPotential + Number:ElectricPotential Actual charge voltage of charger phase L2 Energy From 9c3a25c4d198a288f955be8dc64710ecd2c98ee6 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:30:50 +0200 Subject: [PATCH 15/29] Update bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml Using uppercase for unit hint Co-authored-by: lsiepel Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../src/main/resources/OH-INF/thing/site.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml index 9d2b5dfc78d8f..681a7f21afbc7 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml @@ -240,7 +240,7 @@ - Number:ElectricPotential + Number:ElectricPotential Actual charge voltage of charger phase L2 Energy From 99cc29e36b3710433bb0df6551eea49cb4b8a4b9 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Fri, 24 Oct 2025 23:13:57 +0200 Subject: [PATCH 16/29] Add StateResolve class, fix unmutuable list issue Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../handler/EvccBaseThingHandler.java | 138 ++++-------------- .../internal/handler/EvccBridgeHandler.java | 6 +- .../evcc/internal/handler/StateResolver.java | 99 +++++++++++++ .../handler/BaseThingHandlerTestClass.java | 7 - .../handler/EvccBaseThingHandlerTest.java | 105 ------------- 5 files changed, 135 insertions(+), 220 deletions(-) create mode 100644 bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java index 58ddbe2b12725..747d375bda55e 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java @@ -30,8 +30,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import javax.measure.Unit; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; @@ -39,13 +37,6 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.openhab.core.i18n.TranslationProvider; -import org.openhab.core.library.CoreItemFactory; -import org.openhab.core.library.types.DateTimeType; -import org.openhab.core.library.types.DecimalType; -import org.openhab.core.library.types.OnOffType; -import org.openhab.core.library.types.QuantityType; -import org.openhab.core.library.types.StringType; -import org.openhab.core.library.unit.Units; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; @@ -61,6 +52,7 @@ import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; import org.slf4j.Logger; @@ -83,6 +75,7 @@ public abstract class EvccBaseThingHandler extends BaseThingHandler implements E protected boolean isInitialized = false; protected String endpoint = ""; protected String smartCostType = ""; + protected @Nullable StateResolver stateResolver = StateResolver.getInstance(); public EvccBaseThingHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) { super(thing); @@ -141,31 +134,39 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { String key = Utils.getKeyFromChannelUID(channelUID); Optional.ofNullable(bridgeHandler).ifPresent(handler -> { - JsonObject state = getStateFromCachedState(handler.getCachedEvccState()); - if (!state.isEmpty()) { - JsonElement value = state.get(key); - ItemTypeUnit typeUnit = getItemType(new ChannelTypeUID(BINDING_ID, channelUID.getId())); - if (null != value) { - setItemValue(typeUnit, channelUID, value); - } + JsonObject jsonState = getStateFromCachedState(handler.getCachedEvccState()); + if (!jsonState.isEmpty()) { + JsonElement value = jsonState.get(key); + Optional.ofNullable(stateResolver).ifPresent(resolver -> { + State state = resolver.resolveState(key, value); + if (null != state) { + updateState(channelUID, state); + } + }); } }); } } + private String getItemType(ChannelTypeUID channelTypeUID) { + ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID); + if (null != channelType) { + String itemType = channelType.getItemType(); + return Objects.requireNonNullElse(itemType, "Unknown"); + } + return "Unknown"; + } + @Nullable protected Channel createChannel(String thingKey, JsonElement value) { ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, thingKey); - ItemTypeUnit typeUnit = getItemType(channelTypeUID); - String itemType = typeUnit.itemType; - + String itemType = getItemType(channelTypeUID); if (!"Unknown".equals(itemType)) { String label = getChannelLabel(thingKey); Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), thingKey)).withLabel(label) .withType(channelTypeUID).withAcceptedItemType(itemType).build(); if (getThing().getChannels().stream().noneMatch(c -> c.getUID().equals(channel.getUID()))) { return channel; - // builder.withChannel(channel); } } else { String valString = Objects.requireNonNullElse(value.toString(), "Null"); @@ -175,7 +176,6 @@ protected Channel createChannel(String thingKey, JsonElement value) { } private String getChannelLabel(String thingKey) { - String returnValue = thingKey; @Nullable String tmp = Optional.ofNullable(bridgeHandler).map(handler -> { String labelKey = "channel-type.evcc." + thingKey + ".label"; @@ -184,67 +184,7 @@ private String getChannelLabel(String thingKey) { Locale locale = handler.getLocaleProvider().getLocale(); return tp.getText(ctx.getBundle(), labelKey, thingKey, locale); }).orElse(thingKey); - if (null != tmp) { - returnValue = tmp; - } - return returnValue; - } - - private ItemTypeUnit getItemType(ChannelTypeUID channelTypeUID) { - ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID); - if (null != channelType) { - String itemType = channelType.getItemType(); - if (null != itemType) { - Unit unit = Utils.getUnitFromChannelType(itemType); - return new ItemTypeUnit(channelType, unit); - } - } - return new ItemTypeUnit(channelType, Units.ONE); - } - - protected void setItemValue(ItemTypeUnit itemTypeUnit, ChannelUID channelUID, JsonElement value) { - if (value.isJsonNull() || itemTypeUnit.itemType.isEmpty()) { - return; - } - switch (itemTypeUnit.itemType) { - case CoreItemFactory.NUMBER: - case NUMBER_CURRENCY: - case NUMBER_ENERGY_PRICE: - updateState(channelUID, new DecimalType(value.getAsDouble())); - break; - case NUMBER_DIMENSIONLESS: - case NUMBER_ELECTRIC_CURRENT: - case NUMBER_EMISSION_INTENSITY: - case NUMBER_ENERGY: - case NUMBER_LENGTH: - case NUMBER_POWER: - Double finalValue = "%".equals(itemTypeUnit.unitHint) ? value.getAsDouble() / 100 : value.getAsDouble(); - if (channelUID.getId().contains("capacity") || "km".equals(itemTypeUnit.unitHint)) { - updateState(channelUID, new QuantityType<>(finalValue, itemTypeUnit.unit.multiply(1000))); - } else if ("Wh".equals(itemTypeUnit.unitHint)) { - updateState(channelUID, new QuantityType<>(finalValue, itemTypeUnit.unit.divide(1000))); - } else { - updateState(channelUID, new QuantityType<>(finalValue, itemTypeUnit.unit)); - } - break; - case NUMBER_TIME: - updateState(channelUID, QuantityType.valueOf(value.getAsDouble() + " s")); - break; - case NUMBER_TEMPERATURE: - updateState(channelUID, QuantityType.valueOf(value.getAsDouble() + " " + itemTypeUnit.unitHint)); - break; - case CoreItemFactory.DATETIME: - updateState(channelUID, new DateTimeType(value.getAsString())); - break; - case CoreItemFactory.STRING: - updateState(channelUID, new StringType(value.getAsString())); - break; - case CoreItemFactory.SWITCH: - updateState(channelUID, value.getAsBoolean() ? OnOffType.ON : OnOffType.OFF); - break; - default: - logUnknownChannelXmlAsync(channelUID.getId(), "Hint for type: " + value.toString()); - } + return null != tmp ? tmp : thingKey; } protected String getThingKey(String key) { @@ -260,12 +200,12 @@ protected String getThingKey(String key) { return (type + "-" + Utils.sanitizeChannelID(key)); } - public void updateStatesFromApiResponse(JsonObject state) { - if (!isInitialized || state.isEmpty()) { + public void updateStatesFromApiResponse(JsonObject jsonState) { + if (!isInitialized || jsonState.isEmpty()) { return; } - for (Map.Entry<@Nullable String, @Nullable JsonElement> entry : state.entrySet()) { + for (Map.Entry<@Nullable String, @Nullable JsonElement> entry : jsonState.entrySet()) { String key = entry.getKey(); JsonElement value = entry.getValue(); if (null == key || null == value || !value.isJsonPrimitive()) { @@ -277,7 +217,7 @@ public void updateStatesFromApiResponse(JsonObject state) { Channel existingChannel = getThing().getChannel(channelUID.getId()); if (existingChannel == null) { ThingBuilder builder = editThing(); - List channels = getThing().getChannels(); + List channels = new ArrayList<>(getThing().getChannels()); builder.withoutChannels(channels); @Nullable Channel newChannel = createChannel(thingKey, value); @@ -287,7 +227,12 @@ public void updateStatesFromApiResponse(JsonObject state) { updateThing(builder.withChannels(channels).build()); } } - setItemValue(getItemType(new ChannelTypeUID(BINDING_ID, channelUID.getId())), channelUID, value); + Optional.ofNullable(stateResolver).ifPresent(resolver -> { + State state = resolver.resolveState(key, value); + if (null != state) { + updateState(channelUID, state); + } + }); } updateStatus(ThingStatus.ONLINE); } @@ -383,23 +328,4 @@ protected void logUnknownChannelXml(String key, String itemType) { logger.error("Failed to write unknown channel definition to file", e); } } - - protected static class ItemTypeUnit { - private final Unit unit; - private final String unitHint; - private final String itemType; - - public ItemTypeUnit(@Nullable ChannelType type, @Nullable Unit unit) { - if (null == type) { - unitHint = ""; - itemType = "Unknown"; - } else { - String tmp = type.getUnitHint(); - unitHint = null != tmp ? tmp : ""; - tmp = type.getItemType(); - itemType = null != tmp ? tmp : "Unknown"; - } - this.unit = Objects.requireNonNullElse(unit, Units.ONE); - } - } } diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBridgeHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBridgeHandler.java index 41a863acabdc3..3253b2207dbbe 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBridgeHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBridgeHandler.java @@ -155,7 +155,7 @@ private void notifyListeners(JsonObject state) { if (listener instanceof BaseThingHandler handler) { logger.warn("Listener {} couldn't parse evcc state", handler.getThing().getUID(), e); } else { - logger.debug("Listener {} is not instance of BaseThingHandlder", listener, e); + logger.debug("Listener {} is not instance of BaseThingHandler", listener, e); } } } @@ -167,7 +167,9 @@ public JsonObject getCachedEvccState() { public void register(EvccThingLifecycleAware handler) { listeners.addIfAbsent(handler); - Optional.of(lastState).ifPresent(handler::prepareApiResponseForChannelStateUpdate); + if (!lastState.isEmpty()) { + handler.prepareApiResponseForChannelStateUpdate(lastState); + } } public void unregister(EvccThingLifecycleAware handler) { diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java new file mode 100644 index 0000000000000..4c90ce4d72a33 --- /dev/null +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.evcc.internal.handler; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.MetricPrefix; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.types.State; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +/** + * The {@link StateResolver} provides a resolver to determine the appropriate State type based on the JSON value and key + * + * @author Marcel Goerentz - Initial contribution + */ +@NonNullByDefault +public final class StateResolver { + + private static final StateResolver INSTANCE = new StateResolver(); + + public static StateResolver getInstance() { + return INSTANCE; + } + + /** + * Liefert QuantityType mit originalem numerischen Wert und (ggf.) präfixierter Unit. + * Gibt null zurück, wenn value nicht numerisch ist. + */ + @Nullable + public State resolveState(String key, JsonElement value) { + if (value.isJsonNull() || !value.isJsonPrimitive()) + return null; + JsonPrimitive prim = value.getAsJsonPrimitive(); + if (prim.isNumber()) { + double raw = prim.getAsDouble(); + Unit base = determineBaseUnitFromKey(key); + if (key.contains("odometer") || key.contains("range") || key.contains("capacity")) { + return new QuantityType<>(raw, MetricPrefix.KILO(base)); + } + return new QuantityType<>(raw, base); + } else if (prim.isString()) { + if (key.contains("timestamp")) { + return new DateTimeType(value.getAsString()); + } else { + return new StringType(value.getAsString()); + } + } else if (prim.isBoolean()) { + return value.getAsBoolean() ? OnOffType.ON : OnOffType.OFF; + } else { + return null; + } + } + + /* ----- Heuristik zur Basiseinheit ----- */ + private Unit determineBaseUnitFromKey(String key) { + String lower = key.toLowerCase(); + + if (lower.contains("soc") || lower.contains("percentage")) + return Units.PERCENT; + if (lower.contains("power") || lower.contains("threshold")) + return Units.WATT; + if (lower.contains("energy") || lower.contains("capacity")) + return Units.WATT_HOUR; + if (lower.contains("temp") || lower.contains("temperature") || lower.contains("heating")) + return SIUnits.CELSIUS; + if (lower.contains("voltage")) + return Units.VOLT; + if (lower.contains("current")) + return Units.AMPERE; + if (lower.contains("duration") || lower.contains("time") || lower.contains("delay") + || lower.contains("remaining")) + return Units.SECOND; + if (lower.contains("odometer") || lower.contains("distance")) + return SIUnits.METRE; + if (lower.contains("co2") || lower.contains("co2perkwh")) + return Units.GRAM_PER_KILOWATT_HOUR; + return Units.ONE; + } +} diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/BaseThingHandlerTestClass.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/BaseThingHandlerTestClass.java index 606b3a469f8ce..ae69dcf525e31 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/BaseThingHandlerTestClass.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/BaseThingHandlerTestClass.java @@ -33,7 +33,6 @@ */ @NonNullByDefault public class BaseThingHandlerTestClass extends EvccBaseThingHandler { - public boolean setItemValueCalled = false; public boolean createChannelCalled = false; public boolean updateThingCalled = false; public boolean updateStatusCalled = false; @@ -66,12 +65,6 @@ protected Channel createChannel(String thingKey, JsonElement value) { return super.createChannel(thingKey, value); } - @Override - protected void setItemValue(ItemTypeUnit itemTypeUnit, ChannelUID channelUID, JsonElement value) { - setItemValueCalled = true; - super.setItemValue(itemTypeUnit, channelUID, value); - } - @Override public void prepareApiResponseForChannelStateUpdate(JsonObject state) { prepareApiResponseForChannelStateUpdateCalled = true; diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java index d7c66c8e72bc2..b1891344615e2 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java @@ -17,36 +17,16 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_CURRENCY; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_DIMENSIONLESS; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_ELECTRIC_CURRENT; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_EMISSION_INTENSITY; import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_ENERGY; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_ENERGY_PRICE; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_LENGTH; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_POWER; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_TEMPERATURE; -import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_TIME; import java.util.ArrayList; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.openhab.binding.evcc.internal.handler.EvccBaseThingHandler.ItemTypeUnit; -import org.openhab.core.library.CoreItemFactory; -import org.openhab.core.library.types.DecimalType; -import org.openhab.core.library.types.OnOffType; -import org.openhab.core.library.types.QuantityType; -import org.openhab.core.library.types.StringType; -import org.openhab.core.library.unit.Units; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -59,7 +39,6 @@ import org.openhab.core.types.RefreshType; import com.google.gson.JsonElement; -import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; @@ -96,7 +75,6 @@ public void updateFromEvccStateNotInitializedDoesNothing() { JsonObject state = new JsonObject(); handler.updateStatesFromApiResponse(state); assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); - assertFalse(handler.setItemValueCalled); assertFalse(handler.createChannelCalled); assertFalse(handler.updateThingCalled); assertFalse(handler.updateStatusCalled); @@ -109,7 +87,6 @@ public void updateFromEvccStateEmptyStateDoesNothing() { JsonObject state = new JsonObject(); handler.updateStatesFromApiResponse(state); assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); - assertFalse(handler.setItemValueCalled); assertFalse(handler.createChannelCalled); assertFalse(handler.updateThingCalled); // Status should not be updated for empty state @@ -132,7 +109,6 @@ public void updateFromEvccStateWithPrimitiveValueCreatesChannelAndSetsItemValue( handler.updateStatesFromApiResponse(state); assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); assertTrue(handler.createChannelCalled); - assertTrue(handler.setItemValueCalled); assertTrue(handler.updateThingCalled); assertTrue(handler.updateStatusCalled); assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); @@ -151,7 +127,6 @@ public void updateFromEvccStateWithExistingChannelDoesNotCreateChannel() { assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); assertFalse(handler.createChannelCalled); - assertTrue(handler.setItemValueCalled); assertFalse(handler.updateThingCalled); // Should not update thing if channel exists assertTrue(handler.updateStatusCalled); assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); @@ -169,7 +144,6 @@ public void updateFromEvccStateSkipsNonPrimitiveValues() { assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled); assertFalse(handler.createChannelCalled); - assertFalse(handler.setItemValueCalled); assertFalse(handler.updateThingCalled); assertTrue(handler.updateStatusCalled); // Status is updated even if nothing else happens assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); @@ -182,7 +156,6 @@ void updateStatesFromApiResponseWithNullValueDoesNothing() { state.add("capacity", null); // Null value handler.updateStatesFromApiResponse(state); assertFalse(handler.createChannelCalled); - assertFalse(handler.setItemValueCalled); assertFalse(handler.updateThingCalled); assertTrue(handler.updateStatusCalled); assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus); @@ -208,8 +181,6 @@ public void handleCommandWithNumberItemType() { handler.bridgeHandler = mock(EvccBridgeHandler.class); handler.handleCommand(channelUID, command); - - assertTrue(handler.setItemValueCalled); } @SuppressWarnings("null") @@ -229,8 +200,6 @@ public void handleCommandWithRefreshTypeAndValidValue() { handler.bridgeHandler = mock(EvccBridgeHandler.class); handler.handleCommand(channelUID, command); - - assertTrue(handler.setItemValueCalled); } @SuppressWarnings("null") @@ -243,8 +212,6 @@ public void handleCommandWithRefreshTypeAndMissingValue() { doReturn(cachedState).when(handler).getStateFromCachedState(any()); handler.handleCommand(channelUID, command); - - assertFalse(handler.setItemValueCalled); assertFalse(handler.logUnknownChannelXmlCalled); } @@ -254,7 +221,6 @@ void handleCommandWithNonRefreshTypeDoesNothing() { ChannelUID channelUID = new ChannelUID("test:thing:uid:battery-capacity"); Command command = mock(org.openhab.core.types.Command.class); handler.handleCommand(channelUID, command); - assertFalse(handler.setItemValueCalled); } } @@ -308,77 +274,6 @@ public void getThingKeyWithDefaultType() { } } - @Nested - class SetItemValueTests { - - static Stream provideItemTypesWithExpectedStateClass() { - return Stream.of(Arguments.of(NUMBER_DIMENSIONLESS, QuantityType.class), - Arguments.of(NUMBER_ELECTRIC_CURRENT, QuantityType.class), - Arguments.of(NUMBER_EMISSION_INTENSITY, QuantityType.class), - Arguments.of(NUMBER_ENERGY, QuantityType.class), Arguments.of(NUMBER_LENGTH, QuantityType.class), - Arguments.of(NUMBER_POWER, QuantityType.class), Arguments.of(NUMBER_TIME, QuantityType.class), - Arguments.of(NUMBER_TEMPERATURE, QuantityType.class), - Arguments.of(CoreItemFactory.NUMBER, DecimalType.class), - Arguments.of(NUMBER_CURRENCY, DecimalType.class), - Arguments.of(NUMBER_ENERGY_PRICE, DecimalType.class), - Arguments.of(CoreItemFactory.STRING, StringType.class), - Arguments.of(CoreItemFactory.SWITCH, OnOffType.class)); - } - - @SuppressWarnings("null") - @ParameterizedTest - @MethodSource("provideItemTypesWithExpectedStateClass") - void setItemValueWithVariousTypes(String itemType, Class expectedStateClass) { - ChannelUID channelUID = new ChannelUID("test:thing:uid:dummy"); - JsonElement value = itemType.equals(CoreItemFactory.STRING) ? new JsonPrimitive("OK") - : itemType.equals(CoreItemFactory.SWITCH) ? new JsonPrimitive(true) : new JsonPrimitive(12.5); - - ChannelType mockChannelType = mock(ChannelType.class); - when(mockChannelType.getItemType()).thenReturn(itemType); - if (NUMBER_TEMPERATURE.equals(itemType)) { - when(mockChannelType.getUnitHint()).thenReturn("°C"); - } - when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType); - - ItemTypeUnit itemTypeUnit = new ItemTypeUnit(mockChannelType, Units.ONE); - - handler.setItemValue(itemTypeUnit, channelUID, value); - - assertTrue(handler.updateStateCalled); - assertEquals(channelUID, handler.lastChannelUID); - assertEquals(expectedStateClass, handler.lastState.getClass()); - } - - @SuppressWarnings("null") - @Test - void setItemValueWithUnknownItemTypeDoesNotUpdateState() { - ChannelUID channelUID = new ChannelUID("test:thing:uid:dummy"); - JsonElement value = new JsonPrimitive(12.5); - ChannelType mockChannelType = mock(ChannelType.class); - when(mockChannelType.getItemType()).thenReturn("Unknown"); - ItemTypeUnit itemTypeUnit = new ItemTypeUnit(mockChannelType, Units.ONE); - - handler.setItemValue(itemTypeUnit, channelUID, value); - - assertFalse(handler.updateStateCalled); - assertTrue(handler.logUnknownChannelXmlCalled); - } - - @SuppressWarnings("null") - @Test - void setItemValueWithJsonNullDoesNotUpdateState() { - ChannelUID channelUID = new ChannelUID("test:thing:uid:dummy"); - JsonElement value = JsonNull.INSTANCE; - ChannelType mockChannelType = mock(ChannelType.class); - when(mockChannelType.getItemType()).thenReturn(CoreItemFactory.NUMBER); - ItemTypeUnit itemTypeUnit = new ItemTypeUnit(mockChannelType, Units.ONE); - - handler.setItemValue(itemTypeUnit, channelUID, value); - - assertFalse(handler.updateStateCalled); - } - } - @SuppressWarnings("null") @Test void logUnknownChannelXmlAsyncIsCalledAsynchronously() { From e1a82ccc9ec24c51b604dfc4439c32d5cc8ca8cf Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Sun, 26 Oct 2025 20:53:30 +0100 Subject: [PATCH 17/29] Fix channels are not getting updated correctly Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../evcc/internal/handler/StateResolver.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java index 4c90ce4d72a33..0871c400109d4 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java @@ -17,6 +17,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; @@ -54,12 +55,14 @@ public State resolveState(String key, JsonElement value) { if (prim.isNumber()) { double raw = prim.getAsDouble(); Unit base = determineBaseUnitFromKey(key); - if (key.contains("odometer") || key.contains("range") || key.contains("capacity")) { + if (key.contains("Odometer") || key.contains("Range") || key.contains("Capacity")) { return new QuantityType<>(raw, MetricPrefix.KILO(base)); + } else if (key.contains("Price") || key.contains("Tariff") || key.contains("tariff")) { + return new DecimalType(value.getAsDouble()); } return new QuantityType<>(raw, base); } else if (prim.isString()) { - if (key.contains("timestamp")) { + if (key.contains("Timestamp")) { return new DateTimeType(value.getAsString()); } else { return new StringType(value.getAsString()); @@ -77,9 +80,9 @@ private Unit determineBaseUnitFromKey(String key) { if (lower.contains("soc") || lower.contains("percentage")) return Units.PERCENT; - if (lower.contains("power") || lower.contains("threshold")) + if (lower.contains("power") || lower.contains("threshold") || lower.contains("tariffsolar")) return Units.WATT; - if (lower.contains("energy") || lower.contains("capacity")) + if (lower.contains("energy") || lower.contains("capacity") || lower.contains("import")) return Units.WATT_HOUR; if (lower.contains("temp") || lower.contains("temperature") || lower.contains("heating")) return SIUnits.CELSIUS; @@ -88,11 +91,11 @@ private Unit determineBaseUnitFromKey(String key) { if (lower.contains("current")) return Units.AMPERE; if (lower.contains("duration") || lower.contains("time") || lower.contains("delay") - || lower.contains("remaining")) + || lower.contains("remaining") || lower.contains("overrun") || lower.contains("precondition")) return Units.SECOND; - if (lower.contains("odometer") || lower.contains("distance")) + if (lower.contains("odometer") || lower.contains("distance") || lower.contains("range")) return SIUnits.METRE; - if (lower.contains("co2") || lower.contains("co2perkwh")) + if (lower.contains("co2")) return Units.GRAM_PER_KILOWATT_HOUR; return Units.ONE; } From 6e8fb0beed446d220c1c79d72ec08ad8dff8ce15 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Sun, 26 Oct 2025 21:19:05 +0100 Subject: [PATCH 18/29] Update documentation for StateResolver Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../evcc/internal/handler/StateResolver.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java index 0871c400109d4..80e9f3f6f2ce3 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java @@ -44,8 +44,16 @@ public static StateResolver getInstance() { } /** - * Liefert QuantityType mit originalem numerischen Wert und (ggf.) präfixierter Unit. - * Gibt null zurück, wenn value nicht numerisch ist. + * Resolves a {@link State} object from a given JSON element based on the provided key. + *

+ * This method interprets the JSON value heuristically, mapping numeric values to + * {@link QuantityType} or {@link DecimalType}, strings to {@link StringType} or {@link DateTimeType}, + * and booleans to {@link OnOffType}. The key is used to infer the appropriate unit of measurement + * or semantic meaning. + * + * @param key the semantic key associated with the value (e.g. "Odometer", "Timestamp", "Price") + * @param value the JSON element to be converted into an openHAB {@link State} + * @return the resolved {@link State}, or {@code null} if the value is not a supported primitive */ @Nullable public State resolveState(String key, JsonElement value) { @@ -58,7 +66,7 @@ public State resolveState(String key, JsonElement value) { if (key.contains("Odometer") || key.contains("Range") || key.contains("Capacity")) { return new QuantityType<>(raw, MetricPrefix.KILO(base)); } else if (key.contains("Price") || key.contains("Tariff") || key.contains("tariff")) { - return new DecimalType(value.getAsDouble()); + return new DecimalType(raw); } return new QuantityType<>(raw, base); } else if (prim.isString()) { @@ -74,7 +82,15 @@ public State resolveState(String key, JsonElement value) { } } - /* ----- Heuristik zur Basiseinheit ----- */ + /** + * Determines the most appropriate base {@link Units} for a given key using keyword heuristics. + * This method performs a case-insensitive analysis of the key to infer the expected unit + * (e.g. "temperature" → °C, "energy" → Wh). It is used to assign meaningful units to numeric values + * when constructing {@link QuantityType} instances. + * + * @param key the semantic key to analyze (e.g. "chargingPower", "batterySoc", "sessionEnergy") + * @return the inferred {@link Units}/{@link SIUnits}, or {@link Units#ONE} if no specific unit is matched + */ private Unit determineBaseUnitFromKey(String key) { String lower = key.toLowerCase(); From e17ca7e02bb0ea74d71b5c3122970bc8328bddda Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Sun, 26 Oct 2025 21:21:37 +0100 Subject: [PATCH 19/29] Fix violations Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../openhab/binding/evcc/internal/handler/StateResolver.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java index 80e9f3f6f2ce3..fc64f56d9cbfd 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java @@ -45,13 +45,12 @@ public static StateResolver getInstance() { /** * Resolves a {@link State} object from a given JSON element based on the provided key. - *

* This method interprets the JSON value heuristically, mapping numeric values to * {@link QuantityType} or {@link DecimalType}, strings to {@link StringType} or {@link DateTimeType}, * and booleans to {@link OnOffType}. The key is used to infer the appropriate unit of measurement * or semantic meaning. * - * @param key the semantic key associated with the value (e.g. "Odometer", "Timestamp", "Price") + * @param key the semantic key associated with the value (e.g. "Odometer", "Timestamp", "Price") * @param value the JSON element to be converted into an openHAB {@link State} * @return the resolved {@link State}, or {@code null} if the value is not a supported primitive */ From 8a258d3815bdf98b0daa849ecc63463635c1d09e Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Fri, 31 Oct 2025 21:34:07 +0100 Subject: [PATCH 20/29] Fix issue where phases configured API endpoint was not used correctly, fix issue in stateResolver, enhance things for file confiuration and update README.md Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- bundles/org.openhab.binding.evcc/README.md | 17 +++++++++----- ...tion.java => EvccBridgeConfiguration.java} | 4 ++-- .../mapper/BatteryDiscoveryMapper.java | 3 +-- .../mapper/LoadpointDiscoveryMapper.java | 2 -- .../discovery/mapper/PvDiscoveryMapper.java | 3 +-- .../discovery/mapper/SiteDiscoveryMapper.java | 4 ++-- .../mapper/VehicleDiscoveryMapper.java | 4 ++-- .../handler/EvccBaseThingHandler.java | 22 ++++++++++++++----- .../internal/handler/EvccBatteryHandler.java | 13 +++++++---- .../internal/handler/EvccBridgeHandler.java | 4 ++-- .../handler/EvccLoadpointHandler.java | 18 +++++++++------ .../evcc/internal/handler/EvccPvHandler.java | 13 +++++++---- .../internal/handler/EvccSiteHandler.java | 1 + .../handler/EvccStatisticsHandler.java | 1 + .../internal/handler/EvccVehicleHandler.java | 8 ++++++- .../evcc/internal/handler/StateResolver.java | 3 ++- .../resources/OH-INF/i18n/evcc.properties | 14 ++++++++---- .../main/resources/OH-INF/thing/battery.xml | 6 +++++ .../main/resources/OH-INF/thing/heating.xml | 6 +++++ .../main/resources/OH-INF/thing/loadpoint.xml | 6 +++++ .../src/main/resources/OH-INF/thing/pv.xml | 6 +++++ .../main/resources/OH-INF/thing/vehicle.xml | 6 +++++ .../AbstractThingHandlerTestClass.java | 5 +++++ .../handler/EvccBaseThingHandlerTest.java | 11 +++++++--- .../handler/EvccBatteryHandlerTest.java | 5 +++++ .../handler/EvccHeatingHandlerTest.java | 5 +++++ .../handler/EvccLoadpointHandlerTest.java | 5 +++++ .../internal/handler/EvccPvHandlerTest.java | 5 +++++ .../handler/EvccVehicleHandlerTest.java | 5 +++++ 29 files changed, 156 insertions(+), 49 deletions(-) rename bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/{EvccConfiguration.java => EvccBridgeConfiguration.java} (83%) diff --git a/bundles/org.openhab.binding.evcc/README.md b/bundles/org.openhab.binding.evcc/README.md index 5f96a3e6cc372..f9a349f63035f 100644 --- a/bundles/org.openhab.binding.evcc/README.md +++ b/bundles/org.openhab.binding.evcc/README.md @@ -55,20 +55,25 @@ These channels are dynamically added to the Thing during their initialization; t ### `demo.things` Example ```java -Bridge evcc:server:demo-server "Demo" [scheme="https", url="demo.evcc.io", port=80, refreshInterval=30] { +Bridge evcc:server:demo-server "Demo" [scheme="https", host="demo.evcc.io", port=443, refreshInterval=30] { // This thing will only exist once per evcc instance - Thing site demo-site "Site - evcc Demo" + Thing site + demo-site "Site - evcc Demo" // You can define as many Battery things as you have batteries configured in your evcc instance - Thing battery demo-battery1 "Battery - evcc Demo Battery 1" + Thing battery + demo-battery1 "Battery - evcc Demo Battery 1"[index=0] .. // You can define as many PV things as you have photovoltaics configured in your evcc instance - Thing pv demo-pv1 "PV - evcc Demo Photovoltaic 1" + Thing pv + demo-pv1 "PV - evcc Demo Photovoltaic 1"[index=0] .. // You can define as many Loadpoint things as you have loadpoints configured in your evcc instance - Thing loadpoint demo-loadpoint-carport "Loadpoint - evcc Demo Loadpoint 1" + Thing loadpoint + demo-loadpoint-carport "Loadpoint - evcc Demo Loadpoint 1"[index=0] .. // You can define as many Vehicle things as you have vehicles configured in your evcc instance - Thing vehicle demo-vehicle1 "Vehicle - evcc Demo Vehicle 1" + Thing vehicle + demo-vehicle1 "Vehicle - evcc Demo Vehicle 1"[id="vehicle_1"] .. } ``` diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/EvccConfiguration.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/EvccBridgeConfiguration.java similarity index 83% rename from bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/EvccConfiguration.java rename to bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/EvccBridgeConfiguration.java index 476034f5422ce..f9baa3dabf671 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/EvccConfiguration.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/EvccBridgeConfiguration.java @@ -15,13 +15,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The {@link EvccConfiguration} class contains fields mapping thing configuration parameters. + * The {@link EvccBridgeConfiguration} class contains fields mapping thing configuration parameters. * * @author Florian Hotze - Initial contribution * @author Marcel Goerentz - Rework the binding */ @NonNullByDefault -public class EvccConfiguration { +public class EvccBridgeConfiguration { public String host = ""; public int pollInterval = 30; public int port = 7070; 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 01e5e7964c738..ceb993f78ca37 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 @@ -55,8 +55,7 @@ public Collection discover(JsonObject state, EvccBridgeHandler Utils.sanitizeName(title)); DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(title) .withBridge(bridgeHandler.getThing().getUID()).withProperty(PROPERTY_INDEX, i) - .withProperty(PROPERTY_TYPE, PROPERTY_TYPE_BATTERY).withProperty(PROPERTY_TITLE, title) - .withRepresentationProperty(PROPERTY_TITLE).build(); + .withProperty(PROPERTY_TITLE, title).withRepresentationProperty(PROPERTY_TITLE).build(); results.add(result); } return results; diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/LoadpointDiscoveryMapper.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/LoadpointDiscoveryMapper.java index f113528e4cf0f..740a3610fc72c 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/LoadpointDiscoveryMapper.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/LoadpointDiscoveryMapper.java @@ -60,11 +60,9 @@ public Collection discover(JsonObject state, EvccBridgeHandler if (lp.has(JSON_KEY_CHARGER_FEATURE_HEATING) && lp.get(JSON_KEY_CHARGER_FEATURE_HEATING).getAsBoolean()) { uid = new ThingUID(EvccBindingConstants.THING_TYPE_HEATING, bridgeHandler.getThing().getUID(), Utils.sanitizeName(title)); - properties.put(PROPERTY_TYPE, PROPERTY_TYPE_HEATING); } else { uid = new ThingUID(EvccBindingConstants.THING_TYPE_LOADPOINT, bridgeHandler.getThing().getUID(), Utils.sanitizeName(title)); - properties.put(PROPERTY_TYPE, PROPERTY_TYPE_LOADPOINT); } DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(title) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/PvDiscoveryMapper.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/PvDiscoveryMapper.java index 9c9ef725272fb..11c10abcf41d0 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/PvDiscoveryMapper.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/PvDiscoveryMapper.java @@ -54,8 +54,7 @@ public Collection discover(JsonObject state, EvccBridgeHandler Utils.sanitizeName(title)); DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(title) .withBridge(bridgeHandler.getThing().getUID()).withProperty(PROPERTY_INDEX, i) - .withProperty(PROPERTY_TYPE, PROPERTY_TYPE_PV).withProperty(PROPERTY_TITLE, title) - .withRepresentationProperty(PROPERTY_TITLE).build(); + .withProperty(PROPERTY_TITLE, title).withRepresentationProperty(PROPERTY_TITLE).build(); results.add(result); } diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/SiteDiscoveryMapper.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/SiteDiscoveryMapper.java index 3dddbd95182f8..4ed32b5421c40 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/SiteDiscoveryMapper.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/SiteDiscoveryMapper.java @@ -44,8 +44,8 @@ public Collection discover(JsonObject state, EvccBridgeHandler String siteTitle = state.get("siteTitle").getAsString(); ThingUID uid = new ThingUID(EvccBindingConstants.THING_TYPE_SITE, bridgeHandler.getThing().getUID(), "site"); DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(siteTitle) - .withBridge(bridgeHandler.getThing().getUID()).withProperty(PROPERTY_TYPE, PROPERTY_TYPE_SITE) - .withProperty(PROPERTY_SITE_TITLE, siteTitle).withRepresentationProperty(PROPERTY_SITE_TITLE).build(); + .withBridge(bridgeHandler.getThing().getUID()).withProperty(PROPERTY_SITE_TITLE, siteTitle) + .withRepresentationProperty(PROPERTY_SITE_TITLE).build(); results.add(result); return results; } diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/VehicleDiscoveryMapper.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/VehicleDiscoveryMapper.java index fda872df8291f..f1bcb3c6563f4 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/VehicleDiscoveryMapper.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/discovery/mapper/VehicleDiscoveryMapper.java @@ -53,8 +53,8 @@ public Collection discover(JsonObject state, EvccBridgeHandler ThingUID uid = new ThingUID(EvccBindingConstants.THING_TYPE_VEHICLE, bridgeHandler.getThing().getUID(), Utils.sanitizeName(title)); DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(title) - .withBridge(bridgeHandler.getThing().getUID()).withProperty(PROPERTY_TYPE, PROPERTY_TYPE_VEHICLE) - .withProperty(PROPERTY_ID, id).withRepresentationProperty(PROPERTY_ID).build(); + .withBridge(bridgeHandler.getThing().getUID()).withProperty(PROPERTY_ID, id) + .withRepresentationProperty(PROPERTY_ID).build(); results.add(result); } diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java index 747d375bda55e..dc7bb3540ca7f 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java @@ -58,6 +58,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -71,6 +72,8 @@ public abstract class EvccBaseThingHandler extends BaseThingHandler implements E private final Logger logger = LoggerFactory.getLogger(EvccBaseThingHandler.class); private final ChannelTypeRegistry channelTypeRegistry; + private final Gson gson = new Gson(); + protected String type = ""; protected @Nullable EvccBridgeHandler bridgeHandler; protected boolean isInitialized = false; protected String endpoint = ""; @@ -188,7 +191,6 @@ private String getChannelLabel(String thingKey) { } protected String getThingKey(String key) { - Map props = getThing().getProperties(); if ("batteryGridChargeLimit".equals(key) || "smartCostLimit".equals(key)) { if ("co2".equals(smartCostType)) { key += "Co2"; @@ -196,7 +198,7 @@ protected String getThingKey(String key) { key += "Price"; } } - String type = "heating".equals(props.get("type")) ? "loadpoint" : props.get("type"); + String type = "heating".equals(this.type) ? "loadpoint" : this.type; return (type + "-" + Utils.sanitizeChannelID(key)); } @@ -247,9 +249,19 @@ protected void sendCommand(String url) { if (response.getStatus() == 200) { logger.debug("Sending command was successful"); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); - logger.debug("Sending command was unsuccessful, got this error:\n {}", - response.getContentAsString()); + @Nullable + JsonObject responseJson = gson.fromJson(response.getContentAsString(), JsonObject.class); + Optional.ofNullable(responseJson).ifPresent(json -> { + if (json.has("error")) { + logger.debug("Sending command was unsuccessful, got this error:\n {}", + json.get("error").getAsString()); + updateStatus(getThing().getStatus(), ThingStatusDetail.COMMUNICATION_ERROR, + json.get("error").getAsString()); + } else { + updateStatus(getThing().getStatus(), ThingStatusDetail.COMMUNICATION_ERROR); + logger.warn("evcc API error: HTTP {}", response.getStatus()); + } + }); } } catch (Exception e) { logger.warn("evcc bridge couldn't call the API", e); 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 b13607807fe20..ece086e7bc139 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 @@ -14,7 +14,6 @@ import static org.openhab.binding.evcc.internal.EvccBindingConstants.*; -import java.util.Map; import java.util.Optional; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -37,9 +36,15 @@ public class EvccBatteryHandler extends EvccBaseThingHandler { public EvccBatteryHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) { super(thing, channelTypeRegistry); - Map props = thing.getProperties(); - String indexString = props.getOrDefault(PROPERTY_INDEX, "0"); - index = Integer.parseInt(indexString); + Object index = thing.getConfiguration().get(PROPERTY_INDEX); + String indexString; + if (index instanceof String s) { + indexString = s; + } else { + indexString = thing.getProperties().getOrDefault(PROPERTY_INDEX, "0"); + } + this.index = Integer.parseInt(indexString); + type = PROPERTY_TYPE_BATTERY; } @Override diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBridgeHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBridgeHandler.java index 3253b2207dbbe..2471a1a47fb3d 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBridgeHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBridgeHandler.java @@ -24,7 +24,7 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.http.HttpHeader; -import org.openhab.binding.evcc.internal.EvccConfiguration; +import org.openhab.binding.evcc.internal.EvccBridgeConfiguration; import org.openhab.binding.evcc.internal.discovery.EvccDiscoveryService; import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TranslationProvider; @@ -78,7 +78,7 @@ public Collection> getServices() { @Override public void initialize() { - EvccConfiguration config = getConfigAs(EvccConfiguration.class); + EvccBridgeConfiguration config = getConfigAs(EvccBridgeConfiguration.class); endpoint = config.scheme + "://" + config.host + ":" + config.port + "/api/state"; startPolling(config.pollInterval); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java index d630e09c1d503..a18d78e3aea40 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java @@ -50,13 +50,18 @@ public class EvccLoadpointHandler extends EvccBaseThingHandler { Map.entry(JSON_KEY_PHASES, JSON_KEY_PHASES_CONFIGURED), Map.entry(JSON_KEY_CHARGE_CURRENTS, ""), Map.entry(JSON_KEY_CHARGE_VOLTAGES, "")); protected final int index; - private int[] version = {}; public EvccLoadpointHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) { super(thing, channelTypeRegistry); - Map props = thing.getProperties(); - String indexString = props.getOrDefault(PROPERTY_INDEX, "0"); - index = Integer.parseInt(indexString); + Object index = thing.getConfiguration().get(PROPERTY_INDEX); + String indexString; + if (index instanceof String s) { + indexString = s; + } else { + indexString = thing.getProperties().getOrDefault(PROPERTY_INDEX, "0"); + } + this.index = Integer.parseInt(indexString); + type = PROPERTY_TYPE_LOADPOINT; } @Override @@ -85,8 +90,8 @@ public void initialize() { public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof State) { String datapoint = Utils.getKeyFromChannelUID(channelUID).toLowerCase(); - // Backwards compatibility for phasesConfigured - if (JSON_KEY_PHASES_CONFIGURED.equals(datapoint) && version[0] == 0 && version[1] < 200) { + // Correct the datapoint for the API call + if ("phasesconfigured".equals(datapoint)) { datapoint = JSON_KEY_PHASES; } // Special Handling for enable and disable endpoints @@ -114,7 +119,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { @Override public void prepareApiResponseForChannelStateUpdate(JsonObject state) { - version = Utils.convertVersionStringToIntArray(state.get("version").getAsString().split(" ")[0]); state = state.getAsJsonArray(JSON_KEY_LOADPOINTS).get(index).getAsJsonObject(); modifyJSON(state); updateStatesFromApiResponse(state); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java index 55791a2490ae6..6d9612dcda4af 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java @@ -14,7 +14,6 @@ import static org.openhab.binding.evcc.internal.EvccBindingConstants.*; -import java.util.Map; import java.util.Optional; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -37,9 +36,15 @@ public class EvccPvHandler extends EvccBaseThingHandler { public EvccPvHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) { super(thing, channelTypeRegistry); - Map props = thing.getProperties(); - String indexString = props.getOrDefault(PROPERTY_INDEX, "0"); - index = Integer.parseInt(indexString); + Object index = thing.getConfiguration().get(PROPERTY_INDEX); + String indexString; + if (index instanceof String s) { + indexString = s; + } else { + indexString = thing.getProperties().getOrDefault(PROPERTY_INDEX, "0"); + } + this.index = Integer.parseInt(indexString); + type = PROPERTY_TYPE_PV; } @Override 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 9f271566f1c3e..74123165f9922 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 @@ -45,6 +45,7 @@ public class EvccSiteHandler extends EvccBaseThingHandler { public EvccSiteHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) { super(thing, channelTypeRegistry); + type = PROPERTY_TYPE_SITE; } @Override diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java index 50001bbc926dc..9c6d12fcf96e5 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccStatisticsHandler.java @@ -44,6 +44,7 @@ public class EvccStatisticsHandler extends EvccBaseThingHandler { public EvccStatisticsHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) { super(thing, channelTypeRegistry); + type = PROPERTY_TYPE_STATISTICS; } @Override diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java index e742446a9291c..cece9bcc2d94f 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandler.java @@ -46,7 +46,13 @@ public class EvccVehicleHandler extends EvccBaseThingHandler { public EvccVehicleHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) { super(thing, channelTypeRegistry); - vehicleId = thing.getProperties().get(PROPERTY_ID); + Object id = thing.getConfiguration().get(PROPERTY_ID); + if (id instanceof String s) { + vehicleId = s; + } else { + vehicleId = thing.getProperties().getOrDefault(PROPERTY_ID, ""); + } + type = PROPERTY_TYPE_VEHICLE; } @Override diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java index fc64f56d9cbfd..0a7c145a9cbd9 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java @@ -62,7 +62,8 @@ public State resolveState(String key, JsonElement value) { if (prim.isNumber()) { double raw = prim.getAsDouble(); Unit base = determineBaseUnitFromKey(key); - if (key.contains("Odometer") || key.contains("Range") || key.contains("Capacity")) { + if (key.contains("Odometer") || key.contains("Range") || key.contains("Capacity") + || key.contains("limitEnergy")) { return new QuantityType<>(raw, MetricPrefix.KILO(base)); } else if (key.contains("Price") || key.contains("Tariff") || key.contains("tariff")) { return new DecimalType(raw); diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties index f76510e1668de..ab24389c163d8 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/i18n/evcc.properties @@ -32,6 +32,14 @@ thing-type.evcc.vehicle.description = A vehicle configured in your evcc instance # thing types config +thing-type.config.evcc.battery.index.label = Index +thing-type.config.evcc.battery.index.description = The index of the battery in your evcc setup (starting with 0) +thing-type.config.evcc.heating.index.label = Index +thing-type.config.evcc.heating.index.description = The index of the heating loadpoint in your evcc setup (starting with 0) +thing-type.config.evcc.loadpoint.index.label = Index +thing-type.config.evcc.loadpoint.index.description = The index of the loadpoint in your evcc setup (starting with 0) +thing-type.config.evcc.pv.index.label = Index +thing-type.config.evcc.pv.index.description = The index of the photovoltaic system in your evcc setup (starting with 0) thing-type.config.evcc.server.host.label = Hostname or IP thing-type.config.evcc.server.host.description = Address of your evcc instance, for example evcc.local or 192.168.1.2 thing-type.config.evcc.server.pollInterval.label = Polling Interval @@ -42,6 +50,8 @@ thing-type.config.evcc.server.scheme.label = Protocol Scheme thing-type.config.evcc.server.scheme.description = The protocol scheme that should be used to connect to your instance thing-type.config.evcc.server.scheme.option.http = HTTP thing-type.config.evcc.server.scheme.option.https = HTTPS +thing-type.config.evcc.vehicle.id.label = ID +thing-type.config.evcc.vehicle.id.description = The ID of the vehicle in your evcc setup (database ID) # channel group types @@ -155,10 +165,6 @@ channel-type.evcc.loadpoint-enabled.label = Enabled channel-type.evcc.loadpoint-enabled.description = Indicating whether this loadpoint is enabled or not channel-type.evcc.loadpoint-enabled.state.option.ON = Enabled channel-type.evcc.loadpoint-enabled.state.option.OFF = Disabled -channel-type.evcc.loadpoint-enabled.label = Loadpoint Enabled -channel-type.evcc.loadpoint-enabled.description = Indicates whether the charger is locked or released -channel-type.evcc.loadpoint-enabled.state.option.ON = Released -channel-type.evcc.loadpoint-enabled.state.option.OFF = Locked channel-type.evcc.loadpoint-limit-energy.label = Energy Limit channel-type.evcc.loadpoint-limit-energy.description = Energy limit when charging the vehicle channel-type.evcc.loadpoint-limit-soc.label = SoC Limit diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/battery.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/battery.xml index 2a82cb1b63d6e..dc32130833551 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/battery.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/battery.xml @@ -10,6 +10,12 @@ A battery configured in your evcc instance + + + + The index of the battery in your evcc setup (starting with 0) + + Number:Energy diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/heating.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/heating.xml index 81c94bb9b0d0d..3d91e3bac9981 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/heating.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/heating.xml @@ -11,6 +11,12 @@ A heating loadpoint configured in your evcc instance index + + + + The index of the heating loadpoint in your evcc setup (starting with 0) + + Number:Temperature diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml index 0a88bbc1a2d24..df0c07f0e0f0f 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml @@ -10,6 +10,12 @@ A loadpoint configured in your evcc instance index + + + + The index of the loadpoint in your evcc setup (starting with 0) + + Switch diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/pv.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/pv.xml index cd944831efede..8d842bb767123 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/pv.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/pv.xml @@ -10,6 +10,12 @@ A photovoltaic system configured in your evcc instance + + + + The index of the photovoltaic system in your evcc setup (starting with 0) + + diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/vehicle.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/vehicle.xml index 8268a8f353fd8..c2f4828812c8d 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/vehicle.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/vehicle.xml @@ -10,6 +10,12 @@ A vehicle configured in your evcc instance id + + + + The ID of the vehicle in your evcc setup (database ID) + + Number:Energy diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java index be75061bf7c5a..a328b368d265c 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/AbstractThingHandlerTestClass.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; @@ -87,6 +88,10 @@ public void setUp() { when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "battery")); when(thing.getChannels()).thenReturn(new ArrayList<>()); + Configuration configuration = mock(Configuration.class); + when(configuration.get("index")).thenReturn("0"); + when(configuration.get("id")).thenReturn("vehicle_1"); + when(thing.getConfiguration()).thenReturn(configuration); } @Test diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java index b1891344615e2..22530d425993a 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandlerTest.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -65,6 +66,10 @@ public void setUp() { when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "battery")); when(thing.getChannels()).thenReturn(new ArrayList<>()); + Configuration configuration = mock(Configuration.class); + when(configuration.get("index")).thenReturn("0"); + when(configuration.get("id")).thenReturn("vehicle_1"); + when(thing.getConfiguration()).thenReturn(configuration); } @Nested @@ -245,7 +250,7 @@ public void testCreateChannelWithUnknownItemType() { class GetThingKeyTests { @Test public void getThingKeyWithBatteryTypeAndSpecialKey() { - when(thing.getProperties()).thenReturn(Map.of("type", "battery")); + handler.type = "battery"; String key = "soc"; String result = handler.getThingKey(key); @@ -255,7 +260,7 @@ public void getThingKeyWithBatteryTypeAndSpecialKey() { @Test public void getThingKeyWithHeatingType() { - when(thing.getProperties()).thenReturn(Map.of("type", "heating")); + handler.type = "loadpoint"; String key = "capacity"; String result = handler.getThingKey(key); @@ -265,7 +270,7 @@ public void getThingKeyWithHeatingType() { @Test public void getThingKeyWithDefaultType() { - when(thing.getProperties()).thenReturn(Map.of("type", "loadpoint")); + handler.type = "loadpoint"; String key = "someKey"; String result = handler.getThingKey(key); 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 e9ff93007c2d7..5474995c723fb 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 @@ -23,6 +23,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -83,6 +84,10 @@ public void setup() { when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "battery")); when(thing.getChannels()).thenReturn(new ArrayList<>()); + Configuration configuration = mock(Configuration.class); + when(configuration.get("index")).thenReturn("0"); + when(configuration.get("id")).thenReturn("vehicle_1"); + when(thing.getConfiguration()).thenReturn(configuration); handler = spy(createHandler()); EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandlerTest.java index c49c72dafd0da..efeaee1700035 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandlerTest.java @@ -24,6 +24,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -79,6 +80,10 @@ public void setup() { when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "heating")); when(thing.getChannels()).thenReturn(new ArrayList<>()); + Configuration configuration = mock(Configuration.class); + when(configuration.get("index")).thenReturn("0"); + when(configuration.get("id")).thenReturn("vehicle_1"); + when(thing.getConfiguration()).thenReturn(configuration); handler = spy(createHandler()); EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); handler.bridgeHandler = bridgeHandler; diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java index fa1d53254c3f3..b71c02c9deac2 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java @@ -26,6 +26,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -84,6 +85,10 @@ public void setup() { when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "loadpoint")); when(thing.getChannels()).thenReturn(new ArrayList<>()); + Configuration configuration = mock(Configuration.class); + when(configuration.get("index")).thenReturn("0"); + when(configuration.get("id")).thenReturn("vehicle_1"); + when(thing.getConfiguration()).thenReturn(configuration); handler = spy(createHandler()); modifiedVerifyObject.addProperty("chargedEnergy", 50); diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java index c2b7327be6778..c8a7e5f8e7700 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java @@ -24,6 +24,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -81,6 +82,10 @@ public void setup() { when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); when(thing.getProperties()).thenReturn(Map.of("index", "0", "type", "pv")); when(thing.getChannels()).thenReturn(new ArrayList<>()); + Configuration configuration = mock(Configuration.class); + when(configuration.get("index")).thenReturn("0"); + when(configuration.get("id")).thenReturn("vehicle_1"); + when(thing.getConfiguration()).thenReturn(configuration); handler = spy(createHandler()); verifyObject.addProperty("power", 2000); diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandlerTest.java index 3c36a6acfcc77..4396031ec0600 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandlerTest.java @@ -23,6 +23,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -74,6 +75,10 @@ public void setup() { when(thing.getUID()).thenReturn(new ThingUID("test:thing:uid")); when(thing.getProperties()).thenReturn(Map.of("id", "vehicle_1", "type", "vehicle")); when(thing.getChannels()).thenReturn(new ArrayList<>()); + Configuration configuration = mock(Configuration.class); + when(configuration.get("index")).thenReturn("0"); + when(configuration.get("id")).thenReturn("vehicle_1"); + when(thing.getConfiguration()).thenReturn(configuration); handler = spy(createHandler()); EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); handler.bridgeHandler = bridgeHandler; From 10b258aa460e37ebeefb45fcbf8ee1677205d232 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Fri, 31 Oct 2025 21:53:27 +0100 Subject: [PATCH 21/29] Remove warnings from tests Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../evcc/internal/handler/EvccBatteryHandlerTest.java | 6 ++++++ .../evcc/internal/handler/EvccHeatingHandlerTest.java | 6 ++++++ .../evcc/internal/handler/EvccLoadpointHandlerTest.java | 6 ++++++ .../binding/evcc/internal/handler/EvccPvHandlerTest.java | 6 ++++++ .../binding/evcc/internal/handler/EvccSiteHandlerTest.java | 6 ++++++ .../evcc/internal/handler/EvccVehicleHandlerTest.java | 6 ++++++ 6 files changed, 36 insertions(+) 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 5474995c723fb..b32820f23b8a9 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 @@ -25,10 +25,12 @@ import org.junit.jupiter.api.Test; import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingUID; +import org.openhab.core.types.State; import com.google.gson.JsonObject; @@ -70,6 +72,10 @@ protected Bridge getBridge() { @Override public void updateThing(Thing thing) { } + + @Override + protected void updateState(ChannelUID channelUID, State state) { + } }; } diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandlerTest.java index efeaee1700035..ef6718d4b347e 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccHeatingHandlerTest.java @@ -26,10 +26,12 @@ import org.junit.jupiter.api.Test; import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingUID; +import org.openhab.core.types.State; import com.google.gson.JsonObject; @@ -71,6 +73,10 @@ protected Bridge getBridge() { @Override public void updateThing(Thing thing) { } + + @Override + protected void updateState(ChannelUID channelUID, State state) { + } }; } diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java index b71c02c9deac2..4128865ed53d8 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandlerTest.java @@ -28,10 +28,12 @@ import org.junit.jupiter.api.Test; import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingUID; +import org.openhab.core.types.State; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -77,6 +79,10 @@ protected Bridge getBridge() { @Override public void updateThing(Thing thing) { } + + @Override + protected void updateState(ChannelUID channelUID, State state) { + } }; } diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java index c8a7e5f8e7700..47cc47855c970 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccPvHandlerTest.java @@ -26,10 +26,12 @@ import org.junit.jupiter.api.Test; import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingUID; +import org.openhab.core.types.State; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -74,6 +76,10 @@ protected Bridge getBridge() { @Override public void updateThing(Thing thing) { } + + @Override + protected void updateState(ChannelUID channelUID, State state) { + } }; } diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandlerTest.java index fa31129be550f..2e7ae422b3317 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccSiteHandlerTest.java @@ -27,10 +27,12 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingUID; +import org.openhab.core.types.State; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -74,6 +76,10 @@ protected Bridge getBridge() { @Override public void updateThing(Thing thing) { } + + @Override + protected void updateState(ChannelUID channelUID, State state) { + } }; } diff --git a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandlerTest.java b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandlerTest.java index 4396031ec0600..5c248f98510a4 100644 --- a/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandlerTest.java +++ b/bundles/org.openhab.binding.evcc/src/test/java/org/openhab/binding/evcc/internal/handler/EvccVehicleHandlerTest.java @@ -25,10 +25,12 @@ import org.junit.jupiter.api.Test; import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingUID; +import org.openhab.core.types.State; /** * The {@link EvccVehicleHandlerTest} is responsible for testing the EvccSiteHandler implementation @@ -66,6 +68,10 @@ protected Bridge getBridge() { @Override public void updateThing(Thing thing) { } + + @Override + protected void updateState(ChannelUID channelUID, State state) { + } }; } From 4e11abb102ee7b9aca16c963520992e5384f5801 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Sat, 1 Nov 2025 10:11:33 +0100 Subject: [PATCH 22/29] Fix findings from lsiepels review Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- bundles/org.openhab.binding.evcc/README.md | 15 +++++--------- .../handler/EvccBaseThingHandler.java | 3 ++- .../binding/evcc/internal/handler/Utils.java | 20 ------------------- 3 files changed, 7 insertions(+), 31 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/README.md b/bundles/org.openhab.binding.evcc/README.md index f9a349f63035f..9923f1dfb7dc2 100644 --- a/bundles/org.openhab.binding.evcc/README.md +++ b/bundles/org.openhab.binding.evcc/README.md @@ -57,23 +57,18 @@ These channels are dynamically added to the Thing during their initialization; t ```java Bridge evcc:server:demo-server "Demo" [scheme="https", host="demo.evcc.io", port=443, refreshInterval=30] { // This thing will only exist once per evcc instance - Thing site - demo-site "Site - evcc Demo" + Thing site demo-site "Site - evcc Demo" // You can define as many Battery things as you have batteries configured in your evcc instance - Thing battery - demo-battery1 "Battery - evcc Demo Battery 1"[index=0] + Thing battery demo-battery1 "Battery - evcc Demo Battery 1"[index=0] .. // You can define as many PV things as you have photovoltaics configured in your evcc instance - Thing pv - demo-pv1 "PV - evcc Demo Photovoltaic 1"[index=0] + Thing pv demo-pv1 "PV - evcc Demo Photovoltaic 1"[index=0] .. // You can define as many Loadpoint things as you have loadpoints configured in your evcc instance - Thing loadpoint - demo-loadpoint-carport "Loadpoint - evcc Demo Loadpoint 1"[index=0] + Thing loadpoint demo-loadpoint-carport "Loadpoint - evcc Demo Loadpoint 1"[index=0] .. // You can define as many Vehicle things as you have vehicles configured in your evcc instance - Thing vehicle - demo-vehicle1 "Vehicle - evcc Demo Vehicle 1"[id="vehicle_1"] + Thing vehicle demo-vehicle1 "Vehicle - evcc Demo Vehicle 1"[id="vehicle_1"] .. } ``` diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java index dc7bb3540ca7f..d1b12c2a81dab 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccBaseThingHandler.java @@ -13,6 +13,7 @@ package org.openhab.binding.evcc.internal.handler; import static org.openhab.binding.evcc.internal.EvccBindingConstants.*; +import static org.openhab.core.util.StringUtils.capitalize; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -313,7 +314,7 @@ protected void logUnknownChannelXml(String key, String itemType) { veto - """, key, itemType, Utils.capitalizeWords(key)); + """, key, itemType, capitalize(key)); Path filePath = Paths.get(System.getProperty("user.dir"), "evcc", "unknown-channels.xml"); diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java index d7f706777d6e6..8077ebb574b8d 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java @@ -55,26 +55,6 @@ public static Unit getUnitFromChannelType(String itemType) { return Objects.requireNonNullElse(unit, Units.ONE); } - /** - * This method will capitalize the words of a given string - * - * @param input string containing hyphenized words - * @return A string with spaces instead of hyphens and the first letter of each word is capitalized - */ - public static String capitalizeWords(String input) { - String[] allParts = input.split("-"); - String[] parts = Arrays.stream(allParts, 1, allParts.length).toArray(String[]::new); - StringJoiner joiner = new StringJoiner(" "); - - for (String part : parts) { - if (!part.isEmpty()) { - joiner.add(Character.toUpperCase(part.charAt(0)) + part.substring(1)); - } - } - - return joiner.toString(); - } - /** * This method will sanitize a given string for a channel ID * From 5d7483c92e7d5be58ebca49c1c3e62ed1aa4888f Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Sat, 1 Nov 2025 10:55:52 +0100 Subject: [PATCH 23/29] Fix spotless violations Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../java/org/openhab/binding/evcc/internal/handler/Utils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java index 8077ebb574b8d..16fa3392b97a9 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/Utils.java @@ -23,7 +23,6 @@ import java.util.Arrays; import java.util.Map; import java.util.Objects; -import java.util.StringJoiner; import javax.measure.Unit; From 958016a9b8c2f964eb32eb179e8c6641c56d295e Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Sat, 1 Nov 2025 11:01:06 +0100 Subject: [PATCH 24/29] Fix the low issues also Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../evcc/internal/handler/StateResolver.java | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java index 0a7c145a9cbd9..6e7d5b482116b 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java @@ -56,8 +56,9 @@ public static StateResolver getInstance() { */ @Nullable public State resolveState(String key, JsonElement value) { - if (value.isJsonNull() || !value.isJsonPrimitive()) + if (value.isJsonNull() || !value.isJsonPrimitive()) { return null; + } JsonPrimitive prim = value.getAsJsonPrimitive(); if (prim.isNumber()) { double raw = prim.getAsDouble(); @@ -94,25 +95,34 @@ public State resolveState(String key, JsonElement value) { private Unit determineBaseUnitFromKey(String key) { String lower = key.toLowerCase(); - if (lower.contains("soc") || lower.contains("percentage")) + if (lower.contains("soc") || lower.contains("percentage")) { return Units.PERCENT; - if (lower.contains("power") || lower.contains("threshold") || lower.contains("tariffsolar")) + } + if (lower.contains("power") || lower.contains("threshold") || lower.contains("tariffsolar")) { return Units.WATT; - if (lower.contains("energy") || lower.contains("capacity") || lower.contains("import")) + } + if (lower.contains("energy") || lower.contains("capacity") || lower.contains("import")) { return Units.WATT_HOUR; - if (lower.contains("temp") || lower.contains("temperature") || lower.contains("heating")) + } + if (lower.contains("temp") || lower.contains("temperature") || lower.contains("heating")) { return SIUnits.CELSIUS; - if (lower.contains("voltage")) + } + if (lower.contains("voltage")) { return Units.VOLT; - if (lower.contains("current")) + } + if (lower.contains("current")) { return Units.AMPERE; + } if (lower.contains("duration") || lower.contains("time") || lower.contains("delay") - || lower.contains("remaining") || lower.contains("overrun") || lower.contains("precondition")) + || lower.contains("remaining") || lower.contains("overrun") || lower.contains("precondition")) { return Units.SECOND; - if (lower.contains("odometer") || lower.contains("distance") || lower.contains("range")) + } + if (lower.contains("odometer") || lower.contains("distance") || lower.contains("range")) { return SIUnits.METRE; - if (lower.contains("co2")) + } + if (lower.contains("co2")) { return Units.GRAM_PER_KILOWATT_HOUR; + } return Units.ONE; } } From 701b1645b45809b6c2a831e88d60f8826c3dd09b Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Sat, 1 Nov 2025 22:12:32 +0100 Subject: [PATCH 25/29] Fix markvis issues Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../evcc/internal/handler/StateResolver.java | 16 +++++++++++----- .../src/main/resources/OH-INF/thing/site.xml | 6 +++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java index 6e7d5b482116b..5fa91966e3c85 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java @@ -61,13 +61,19 @@ public State resolveState(String key, JsonElement value) { } JsonPrimitive prim = value.getAsJsonPrimitive(); if (prim.isNumber()) { - double raw = prim.getAsDouble(); + String lowerKey = key.toLowerCase(); + Number raw = prim.getAsNumber(); Unit base = determineBaseUnitFromKey(key); - if (key.contains("Odometer") || key.contains("Range") || key.contains("Capacity") - || key.contains("limitEnergy")) { - return new QuantityType<>(raw, MetricPrefix.KILO(base)); - } else if (key.contains("Price") || key.contains("Tariff") || key.contains("tariff")) { + if (lowerKey.contains("price") || key.contains("tariff")) { return new DecimalType(raw); + } else if (raw.toString().contains(".")) { + if ("energy".equals(lowerKey) || "gridenergy".equals(lowerKey) || "chargedenergy".equals(lowerKey) + || "pvenergy".equals(lowerKey) || "chargedkwh".equals(lowerKey) || lowerKey.contains("import") + || lowerKey.contains("capacity")) { + return new QuantityType<>(raw, MetricPrefix.KILO(base)); + } + } else if (lowerKey.contains("capacity") || lowerKey.contains("odometer") || lowerKey.contains("range")) { + return new QuantityType<>(raw, MetricPrefix.KILO(base)); } return new QuantityType<>(raw, base); } else if (prim.isString()) { diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml index 681a7f21afbc7..d59facf44099b 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml @@ -314,14 +314,14 @@ - Number + Number Refresh interval configured for evcc Settings - + - Number:Energy + Number:Energy Accumulated energy provided through photovoltaic systems Energy From 359181466ba5a20d0cc5b6610ad2275848d6eeb6 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Sun, 2 Nov 2025 10:38:53 +0100 Subject: [PATCH 26/29] Add markvis suggestions Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../openhab/binding/evcc/internal/handler/StateResolver.java | 3 ++- .../src/main/resources/OH-INF/thing/loadpoint.xml | 2 +- .../src/main/resources/OH-INF/thing/site.xml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java index 5fa91966e3c85..4f7634969e27d 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java @@ -72,7 +72,8 @@ public State resolveState(String key, JsonElement value) { || lowerKey.contains("capacity")) { return new QuantityType<>(raw, MetricPrefix.KILO(base)); } - } else if (lowerKey.contains("capacity") || lowerKey.contains("odometer") || lowerKey.contains("range")) { + } else if (lowerKey.contains("capacity") || lowerKey.contains("odometer") || lowerKey.contains("range") + || lowerKey.contains("chargetotalimport")) { return new QuantityType<>(raw, MetricPrefix.KILO(base)); } return new QuantityType<>(raw, base); diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml index df0c07f0e0f0f..0e4d84f30c49f 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/loadpoint.xml @@ -194,7 +194,7 @@ Status Mode - + diff --git a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml index d59facf44099b..936087b819fef 100644 --- a/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml +++ b/bundles/org.openhab.binding.evcc/src/main/resources/OH-INF/thing/site.xml @@ -314,7 +314,7 @@ - Number + Number:Time Refresh interval configured for evcc Settings From 4b5e20e4ceb3de43790b31627d4fb8baecd60f07 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Sun, 2 Nov 2025 11:08:55 +0100 Subject: [PATCH 27/29] Fix issue where configuration index was not parsed correctly Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../binding/evcc/internal/handler/EvccBatteryHandler.java | 5 +++-- .../binding/evcc/internal/handler/EvccLoadpointHandler.java | 5 +++-- .../openhab/binding/evcc/internal/handler/EvccPvHandler.java | 5 +++-- .../openhab/binding/evcc/internal/handler/StateResolver.java | 5 +++-- 4 files changed, 12 insertions(+), 8 deletions(-) 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 ece086e7bc139..a78e1fa4c4a25 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 @@ -14,6 +14,7 @@ import static org.openhab.binding.evcc.internal.EvccBindingConstants.*; +import java.math.BigDecimal; import java.util.Optional; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -38,8 +39,8 @@ public EvccBatteryHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) super(thing, channelTypeRegistry); Object index = thing.getConfiguration().get(PROPERTY_INDEX); String indexString; - if (index instanceof String s) { - indexString = s; + if (index instanceof BigDecimal s) { + indexString = s.toString(); } else { indexString = thing.getProperties().getOrDefault(PROPERTY_INDEX, "0"); } diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java index a18d78e3aea40..f609b377e4f09 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccLoadpointHandler.java @@ -14,6 +14,7 @@ import static org.openhab.binding.evcc.internal.EvccBindingConstants.*; +import java.math.BigDecimal; import java.util.Map; import java.util.Optional; @@ -55,8 +56,8 @@ public EvccLoadpointHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry super(thing, channelTypeRegistry); Object index = thing.getConfiguration().get(PROPERTY_INDEX); String indexString; - if (index instanceof String s) { - indexString = s; + if (index instanceof BigDecimal s) { + indexString = s.toString(); } else { indexString = thing.getProperties().getOrDefault(PROPERTY_INDEX, "0"); } diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java index 6d9612dcda4af..d0f7895cf4c4e 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/EvccPvHandler.java @@ -14,6 +14,7 @@ import static org.openhab.binding.evcc.internal.EvccBindingConstants.*; +import java.math.BigDecimal; import java.util.Optional; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -38,8 +39,8 @@ public EvccPvHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) { super(thing, channelTypeRegistry); Object index = thing.getConfiguration().get(PROPERTY_INDEX); String indexString; - if (index instanceof String s) { - indexString = s; + if (index instanceof BigDecimal s) { + indexString = s.toString(); } else { indexString = thing.getProperties().getOrDefault(PROPERTY_INDEX, "0"); } diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java index 4f7634969e27d..b16f10fd11268 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java @@ -73,7 +73,7 @@ public State resolveState(String key, JsonElement value) { return new QuantityType<>(raw, MetricPrefix.KILO(base)); } } else if (lowerKey.contains("capacity") || lowerKey.contains("odometer") || lowerKey.contains("range") - || lowerKey.contains("chargetotalimport")) { + || lowerKey.contains("import")) { return new QuantityType<>(raw, MetricPrefix.KILO(base)); } return new QuantityType<>(raw, base); @@ -121,7 +121,8 @@ private Unit determineBaseUnitFromKey(String key) { return Units.AMPERE; } if (lower.contains("duration") || lower.contains("time") || lower.contains("delay") - || lower.contains("remaining") || lower.contains("overrun") || lower.contains("precondition")) { + || lower.contains("interval") || lower.contains("remaining") || lower.contains("overrun") + || lower.contains("precondition")) { return Units.SECOND; } if (lower.contains("odometer") || lower.contains("distance") || lower.contains("range")) { From c544150981b98abef934523de1b65a3824433096 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Sun, 2 Nov 2025 11:51:59 +0100 Subject: [PATCH 28/29] Fix EvccBatteryHandlerTest Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../handler/EvccBatteryHandlerTest.java | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) 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 b32820f23b8a9..384fcd152c0ed 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.assertSame; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; import java.util.ArrayList; @@ -20,7 +20,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openhab.core.config.core.Configuration; @@ -79,11 +78,6 @@ protected void updateState(ChannelUID channelUID, State state) { }; } - @BeforeAll - static void setUpOnce() { - batteryState = exampleResponse.getAsJsonArray("battery").get(0).getAsJsonObject(); - } - @SuppressWarnings("null") @BeforeEach public void setup() { @@ -99,6 +93,16 @@ public void setup() { EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class); handler.bridgeHandler = bridgeHandler; when(bridgeHandler.getCachedEvccState()).thenReturn(exampleResponse); + batteryState = exampleResponse.getAsJsonArray("battery").get(0).getAsJsonObject(); + } + + @SuppressWarnings("null") + @Test + public void testPrepareApiResponseForChannelStateUpdateIsNotInitialized() { + handler.isInitialized = false; + handler.prepareApiResponseForChannelStateUpdate(exampleResponse); + verify(handler).updateStatesFromApiResponse(batteryState); + assertSame(ThingStatus.UNKNOWN, lastThingStatus); } @SuppressWarnings("null") @@ -116,14 +120,6 @@ public void testPrepareApiResponseForChannelStateUpdateIsInitialized() { assertSame(ThingStatus.ONLINE, lastThingStatus); } - @SuppressWarnings("null") - @Test - public void testPrepareApiResponseForChannelStateUpdateIsNotInitialized() { - handler.prepareApiResponseForChannelStateUpdate(exampleResponse); - verify(handler).updateStatesFromApiResponse(batteryState); - assertSame(ThingStatus.UNKNOWN, lastThingStatus); - } - @SuppressWarnings("null") @Test public void testGetStateFromCachedState() { From ea67653976d36d3fcf78f89f0bd6cb5ca2913e72 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:09:25 +0100 Subject: [PATCH 29/29] Add limitenergy as case to the state resolver Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> --- .../openhab/binding/evcc/internal/handler/StateResolver.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java index b16f10fd11268..a29d025cc5dd7 100644 --- a/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java +++ b/bundles/org.openhab.binding.evcc/src/main/java/org/openhab/binding/evcc/internal/handler/StateResolver.java @@ -68,12 +68,13 @@ public State resolveState(String key, JsonElement value) { return new DecimalType(raw); } else if (raw.toString().contains(".")) { if ("energy".equals(lowerKey) || "gridenergy".equals(lowerKey) || "chargedenergy".equals(lowerKey) - || "pvenergy".equals(lowerKey) || "chargedkwh".equals(lowerKey) || lowerKey.contains("import") + || "pvenergy".equals(lowerKey) || "limitenergy".equals(lowerKey) + || "chargedkwh".equals(lowerKey) || lowerKey.contains("import") || lowerKey.contains("capacity")) { return new QuantityType<>(raw, MetricPrefix.KILO(base)); } } else if (lowerKey.contains("capacity") || lowerKey.contains("odometer") || lowerKey.contains("range") - || lowerKey.contains("import")) { + || lowerKey.contains("import") || "limitenergy".equals(lowerKey)) { return new QuantityType<>(raw, MetricPrefix.KILO(base)); } return new QuantityType<>(raw, base);