[evcc] Add channels for grid currents, grid energy and charging currents. (#19228)

* Add channels for grid currents, grid energy and charging currents.

Signed-off-by: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com>
This commit is contained in:
Marcel Goerentz
2025-11-04 22:50:16 +01:00
committed by GitHub
parent e72df64f18
commit 084cb869eb
41 changed files with 3396 additions and 278 deletions
+5 -5
View File
@@ -55,20 +55,20 @@ 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"
// 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"]
..
}
```
@@ -59,12 +59,31 @@ 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_CHARGE_CURRENT = "chargeCurrent";
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_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_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";
@@ -15,13 +15,13 @@ package org.openhab.binding.evcc.internal;
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;
@@ -67,7 +67,7 @@ public class EvccDiscoveryService extends AbstractThingHandlerDiscoveryService<E
@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 -> {
@@ -41,21 +41,21 @@ public class BatteryDiscoveryMapper implements EvccDiscoveryMapper {
@Override
public Collection<DiscoveryResult> discover(JsonObject state, EvccBridgeHandler bridgeHandler) {
List<DiscoveryResult> 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));
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;
@@ -43,28 +43,26 @@ public class LoadpointDiscoveryMapper implements EvccDiscoveryMapper {
@Override
public Collection<DiscoveryResult> discover(JsonObject state, EvccBridgeHandler bridgeHandler) {
List<DiscoveryResult> 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<String, Object> 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);
} 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)
@@ -41,20 +41,20 @@ public class PvDiscoveryMapper implements EvccDiscoveryMapper {
@Override
public Collection<DiscoveryResult> discover(JsonObject state, EvccBridgeHandler bridgeHandler) {
List<DiscoveryResult> 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));
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);
}
@@ -44,8 +44,8 @@ public class SiteDiscoveryMapper implements EvccDiscoveryMapper {
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;
}
@@ -38,12 +38,12 @@ public class StatisticsDiscoveryMapper implements EvccDiscoveryMapper {
@Override
public Collection<DiscoveryResult> discover(JsonObject state, EvccBridgeHandler bridgeHandler) {
List<DiscoveryResult> 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();
@@ -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,20 +41,20 @@ public class VehicleDiscoveryMapper implements EvccDiscoveryMapper {
@Override
public Collection<DiscoveryResult> discover(JsonObject state, EvccBridgeHandler bridgeHandler) {
List<DiscoveryResult> 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<String, JsonElement> 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));
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);
}
@@ -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;
@@ -20,6 +21,9 @@ import java.nio.file.Files;
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;
@@ -27,8 +31,6 @@ import java.util.Optional;
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;
@@ -36,13 +38,6 @@ import org.eclipse.jetty.client.api.ContentResponse;
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;
@@ -58,11 +53,13 @@ import org.openhab.core.thing.type.ChannelTypeRegistry;
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;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
@@ -76,10 +73,13 @@ 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 = "";
protected String smartCostType = "";
protected @Nullable StateResolver stateResolver = StateResolver.getInstance();
public EvccBaseThingHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) {
super(thing);
@@ -87,7 +87,7 @@ public abstract class EvccBaseThingHandler extends BaseThingHandler implements E
}
protected void commonInitialize(JsonObject state) {
ThingBuilder builder = editThing();
List<Channel> newChannels = new ArrayList<>();
for (Map.Entry<@Nullable String, @Nullable JsonElement> entry : state.entrySet()) {
String key = entry.getKey();
@@ -102,10 +102,15 @@ public abstract class EvccBaseThingHandler extends BaseThingHandler implements E
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),
@@ -133,38 +138,48 @@ public abstract class EvccBaseThingHandler extends BaseThingHandler implements E
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 void createChannel(String thingKey, ThingBuilder builder, JsonElement value) {
ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, thingKey);
ItemTypeUnit typeUnit = getItemType(channelTypeUID);
String itemType = typeUnit.itemType;
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);
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()))) {
builder.withChannel(channel);
return channel;
}
} else {
String valString = Objects.requireNonNullElse(value.toString(), "Null");
logUnknownChannelXmlAsync(thingKey, "Hint for type: " + valString);
}
return null;
}
private String getChannelLabel(String thingKey) {
String returnValue = thingKey;
@Nullable
String tmp = Optional.ofNullable(bridgeHandler).map(handler -> {
String labelKey = "channel-type.evcc." + thingKey + ".label";
@@ -173,71 +188,10 @@ public abstract class EvccBaseThingHandler extends BaseThingHandler implements E
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);
}
private 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) {
Map<String, String> props = getThing().getProperties();
if ("batteryGridChargeLimit".equals(key) || "smartCostLimit".equals(key)) {
if ("co2".equals(smartCostType)) {
key += "Co2";
@@ -245,17 +199,16 @@ public abstract class EvccBaseThingHandler extends BaseThingHandler implements E
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));
}
@Override
public void updateFromEvccState(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()) {
@@ -267,10 +220,22 @@ public abstract class EvccBaseThingHandler extends BaseThingHandler implements E
Channel existingChannel = getThing().getChannel(channelUID.getId());
if (existingChannel == null) {
ThingBuilder builder = editThing();
createChannel(thingKey, builder, value);
updateThing(builder.build());
List<Channel> channels = new ArrayList<>(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);
Optional.ofNullable(stateResolver).ifPresent(resolver -> {
State state = resolver.resolveState(key, value);
if (null != state) {
updateState(channelUID, state);
}
});
}
updateStatus(ThingStatus.ONLINE);
}
@@ -285,9 +250,19 @@ public abstract class EvccBaseThingHandler extends BaseThingHandler implements E
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);
@@ -339,7 +314,7 @@ public abstract class EvccBaseThingHandler extends BaseThingHandler implements E
<state readOnly="true"></state>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
""", key, itemType, Utils.capitalizeWords(key));
""", key, itemType, capitalize(key));
Path filePath = Paths.get(System.getProperty("user.dir"), "evcc", "unknown-channels.xml");
@@ -366,23 +341,4 @@ public abstract class EvccBaseThingHandler extends BaseThingHandler implements E
logger.error("Failed to write unknown channel definition to file", e);
}
}
private 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);
}
}
}
@@ -14,7 +14,7 @@ package org.openhab.binding.evcc.internal.handler;
import static org.openhab.binding.evcc.internal.EvccBindingConstants.*;
import java.util.Map;
import java.math.BigDecimal;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -37,35 +37,41 @@ public class EvccBatteryHandler extends EvccBaseThingHandler {
public EvccBatteryHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) {
super(thing, channelTypeRegistry);
Map<String, String> 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 BigDecimal s) {
indexString = s.toString();
} else {
indexString = thing.getProperties().getOrDefault(PROPERTY_INDEX, "0");
}
this.index = Integer.parseInt(indexString);
type = PROPERTY_TYPE_BATTERY;
}
@Override
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;
}
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 updateFromEvccState(JsonObject state) {
state = state.has(JSON_MEMBER_BATTERY) ? state.getAsJsonArray(JSON_MEMBER_BATTERY).get(index).getAsJsonObject()
public void prepareApiResponseForChannelStateUpdate(JsonObject state) {
state = state.has(JSON_KEY_BATTERY) ? state.getAsJsonArray(JSON_KEY_BATTERY).get(index).getAsJsonObject()
: new JsonObject();
super.updateFromEvccState(state);
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();
}
}
@@ -24,7 +24,7 @@ import org.eclipse.jdt.annotation.Nullable;
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 class EvccBridgeHandler extends BaseBridgeHandler {
@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);
@@ -150,12 +150,12 @@ public class EvccBridgeHandler extends BaseBridgeHandler {
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);
} 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 class EvccBridgeHandler extends BaseBridgeHandler {
public void register(EvccThingLifecycleAware handler) {
listeners.addIfAbsent(handler);
Optional.of(lastState).ifPresent(handler::updateFromEvccState);
if (!lastState.isEmpty()) {
handler.prepareApiResponseForChannelStateUpdate(lastState);
}
}
public void unregister(EvccThingLifecycleAware handler) {
@@ -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.*;
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<String, String> 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);
@@ -69,15 +71,15 @@ public class EvccHeatingHandler extends EvccLoadpointHandler {
}
@Override
public void updateFromEvccState(JsonObject state) {
public void prepareApiResponseForChannelStateUpdate(JsonObject state) {
updateJSON(state);
super.updateFromEvccState(state);
updateStatesFromApiResponse(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) {
@@ -14,6 +14,7 @@ package org.openhab.binding.evcc.internal.handler;
import static org.openhab.binding.evcc.internal.EvccBindingConstants.*;
import java.math.BigDecimal;
import java.util.Map;
import java.util.Optional;
@@ -29,6 +30,8 @@ import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
@@ -40,14 +43,26 @@ import com.google.gson.JsonObject;
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<String, String> 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_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<String, String> 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 BigDecimal s) {
indexString = s.toString();
} else {
indexString = thing.getProperties().getOrDefault(PROPERTY_INDEX, "0");
}
this.index = Integer.parseInt(indexString);
type = PROPERTY_TYPE_LOADPOINT;
}
@Override
@@ -55,7 +70,7 @@ public class EvccLoadpointHandler extends EvccBaseThingHandler {
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;
@@ -65,7 +80,7 @@ public class EvccLoadpointHandler extends EvccBaseThingHandler {
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);
@@ -76,9 +91,9 @@ public class EvccLoadpointHandler extends EvccBaseThingHandler {
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof State) {
String datapoint = Utils.getKeyFromChannelUID(channelUID).toLowerCase();
// Backwardscompatibility for phasesConfigured
if ("configuredPhases".equals(datapoint) && version[0] == 0 && version[1] < 200) {
datapoint = "phases";
// Correct the datapoint for the API call
if ("phasesconfigured".equals(datapoint)) {
datapoint = JSON_KEY_PHASES;
}
// Special Handling for enable and disable endpoints
if (datapoint.contains("enable")) {
@@ -104,34 +119,38 @@ public class EvccLoadpointHandler extends EvccBaseThingHandler {
}
@Override
public void updateFromEvccState(JsonObject state) {
version = Utils.convertVersionStringToIntArray(state.get("version").getAsString().split(" ")[0]);
state = state.getAsJsonArray(JSON_MEMBER_LOADPOINTS).get(index).getAsJsonObject();
public void prepareApiResponseForChannelStateUpdate(JsonObject state) {
state = state.getAsJsonArray(JSON_KEY_LOADPOINTS).get(index).getAsJsonObject();
modifyJSON(state);
super.updateFromEvccState(state);
updateStatesFromApiResponse(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(JSON_KEY_CHARGE_CURRENTS)) {
addMeasurementDatapointToState(state, state.getAsJsonArray(oldKey), "Current");
} else if (oldKey.equals(JSON_KEY_CHARGE_VOLTAGES)) {
addMeasurementDatapointToState(state, state.getAsJsonArray(oldKey), "Voltage");
} else {
state.add(newKey, state.get(oldKey));
}
state.remove(oldKey);
}
});
}
protected void addMeasurementDatapointToState(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)
? state.getAsJsonArray(JSON_MEMBER_LOADPOINTS).get(index).getAsJsonObject()
return state.has(JSON_KEY_LOADPOINTS) ? state.getAsJsonArray(JSON_KEY_LOADPOINTS).get(index).getAsJsonObject()
: new JsonObject();
}
}
@@ -14,7 +14,7 @@ package org.openhab.binding.evcc.internal.handler;
import static org.openhab.binding.evcc.internal.EvccBindingConstants.*;
import java.util.Map;
import java.math.BigDecimal;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -37,35 +37,41 @@ public class EvccPvHandler extends EvccBaseThingHandler {
public EvccPvHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) {
super(thing, channelTypeRegistry);
Map<String, String> 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 BigDecimal s) {
indexString = s.toString();
} else {
indexString = thing.getProperties().getOrDefault(PROPERTY_INDEX, "0");
}
this.index = Integer.parseInt(indexString);
type = PROPERTY_TYPE_PV;
}
@Override
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;
}
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 updateFromEvccState(JsonObject state) {
state = state.getAsJsonArray(JSON_MEMBER_PV).get(index).getAsJsonObject();
super.updateFromEvccState(state);
public void prepareApiResponseForChannelStateUpdate(JsonObject state) {
state = state.getAsJsonArray(JSON_KEY_PV).get(index).getAsJsonObject();
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();
}
}
@@ -12,6 +12,9 @@
*/
package org.openhab.binding.evcc.internal.handler;
import static org.openhab.binding.evcc.internal.EvccBindingConstants.*;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -26,6 +29,8 @@ import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
@@ -40,13 +45,14 @@ public class EvccSiteHandler extends EvccBaseThingHandler {
public EvccSiteHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) {
super(thing, channelTypeRegistry);
type = PROPERTY_TYPE_SITE;
}
@Override
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 {
@@ -64,11 +70,11 @@ public class EvccSiteHandler extends EvccBaseThingHandler {
}
@Override
public void updateFromEvccState(JsonObject state) {
if (state.has("gridConfigured")) {
public void prepareApiResponseForChannelStateUpdate(JsonObject state) {
if (state.has(JSON_KEY_GRID_CONFIGURED)) {
modifyJSON(state);
}
super.updateFromEvccState(state);
updateStatesFromApiResponse(state);
}
@Override
@@ -76,18 +82,18 @@ public class EvccSiteHandler extends EvccBaseThingHandler {
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;
}
// 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);
@@ -95,8 +101,25 @@ public class EvccSiteHandler extends EvccBaseThingHandler {
}
private void modifyJSON(JsonObject state) {
state.add("gridPower", state.getAsJsonObject("grid").get("power"));
state.remove("gridConfigured");
for (Map.Entry<String, JsonElement> 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(JSON_KEY_GRID + Utils.capitalizeFirstLetter(entry.getKey()), entry.getValue());
}
}
state.remove(JSON_KEY_GRID);
state.remove(JSON_KEY_GRID_CONFIGURED);
}
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
@@ -24,6 +24,7 @@ import org.openhab.core.thing.ChannelGroupUID;
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;
@@ -43,6 +44,7 @@ public class EvccStatisticsHandler extends EvccBaseThingHandler {
public EvccStatisticsHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) {
super(thing, channelTypeRegistry);
type = PROPERTY_TYPE_STATISTICS;
}
@Override
@@ -52,6 +54,12 @@ public class EvccStatisticsHandler extends EvccBaseThingHandler {
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);
});
}
@@ -61,8 +69,13 @@ public class EvccStatisticsHandler extends EvccBaseThingHandler {
}
@Override
public void updateFromEvccState(JsonObject state) {
state = state.has(JSON_MEMBER_STATISTICS) ? state.getAsJsonObject(JSON_MEMBER_STATISTICS) : new JsonObject();
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);
@@ -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
@@ -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
@@ -66,9 +72,9 @@ public class EvccVehicleHandler extends EvccBaseThingHandler {
}
@Override
public void updateFromEvccState(JsonObject state) {
state = state.getAsJsonObject(JSON_MEMBER_VEHICLES).getAsJsonObject(vehicleId);
super.updateFromEvccState(state);
public void prepareApiResponseForChannelStateUpdate(JsonObject state) {
state = state.getAsJsonObject(JSON_KEY_VEHICLES).getAsJsonObject(vehicleId);
updateStatesFromApiResponse(state);
}
@Override
@@ -76,20 +82,20 @@ public class EvccVehicleHandler extends EvccBaseThingHandler {
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;
}
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();
}
}
@@ -0,0 +1,137 @@
/*
* 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.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.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;
}
/**
* 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) {
if (value.isJsonNull() || !value.isJsonPrimitive()) {
return null;
}
JsonPrimitive prim = value.getAsJsonPrimitive();
if (prim.isNumber()) {
String lowerKey = key.toLowerCase();
Number raw = prim.getAsNumber();
Unit<?> base = determineBaseUnitFromKey(key);
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) || "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") || "limitenergy".equals(lowerKey)) {
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;
}
}
/**
* 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();
if (lower.contains("soc") || lower.contains("percentage")) {
return Units.PERCENT;
}
if (lower.contains("power") || lower.contains("threshold") || lower.contains("tariffsolar")) {
return Units.WATT;
}
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;
}
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("interval") || lower.contains("remaining") || lower.contains("overrun")
|| lower.contains("precondition")) {
return Units.SECOND;
}
if (lower.contains("odometer") || lower.contains("distance") || lower.contains("range")) {
return SIUnits.METRE;
}
if (lower.contains("co2")) {
return Units.GRAM_PER_KILOWATT_HOUR;
}
return Units.ONE;
}
}
@@ -23,7 +23,6 @@ import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_TIME
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
import javax.measure.Unit;
@@ -55,26 +54,6 @@ public class Utils {
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
*
@@ -122,4 +101,17 @@ public class Utils {
}
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);
}
}
@@ -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
@@ -49,7 +59,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
@@ -68,6 +78,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
@@ -78,6 +94,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
@@ -139,10 +161,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 = 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-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-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
@@ -240,7 +262,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,8 +326,22 @@ 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-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
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
@@ -387,8 +423,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)
@@ -10,6 +10,12 @@
<label>Battery</label>
<description>A battery configured in your evcc instance</description>
<config-description>
<parameter name="index" type="integer">
<label>Index</label>
<description>The index of the battery in your evcc setup (starting with 0)</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="battery-capacity">
<item-type unitHint="kWh">Number:Energy</item-type>
@@ -11,6 +11,12 @@
<label>Heating - Loadpoint</label>
<description>A heating loadpoint configured in your evcc instance</description>
<representation-property>index</representation-property>
<config-description>
<parameter name="index" type="integer">
<label>Index</label>
<description>The index of the heating loadpoint in your evcc setup (starting with 0)</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="loadpoint-effective-limit-temperature">
<item-type unitHint="°C">Number:Temperature</item-type>
@@ -58,7 +64,7 @@
</channel-type>
<channel-type id="loadpoint-vehicle-temperature">
<item-type unitHint="°C">Number:Temperature</item-type>
<label>Temperature</label>
<label>Device Temperature</label>
<description>Current temperature of the heating device</description>
<category>Temperature</category>
<tags>
@@ -10,6 +10,12 @@
<label>Loadpoint</label>
<description>A loadpoint configured in your evcc instance</description>
<representation-property>index</representation-property>
<config-description>
<parameter name="index" type="integer">
<label>Index</label>
<description>The index of the loadpoint in your evcc setup (starting with 0)</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="loadpoint-battery-boost">
<item-type>Switch</item-type>
@@ -188,7 +194,7 @@
<tag>Status</tag>
<tag>Mode</tag>
</tags>
<state pattern="%f %unit%" readOnly="true">
<state readOnly="true">
<options>
<option value="ON">Charging</option>
<option value="OFF">Not charging</option>
@@ -284,6 +290,21 @@
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="loadpoint-enabled">
<item-type>Switch</item-type>
<label>Enabled</label>
<description>Indicating whether this loadpoint is enabled or not</description>
<tags>
<tag>Status</tag>
<tag>Enabled</tag>
</tags>
<state readOnly="true">
<options>
<option value="ON">Enabled</option>
<option value="OFF">Disabled</option>
</options>
</state>
</channel-type>
<channel-type id="loadpoint-plan-projected-end">
<item-type>DateTime</item-type>
<label>Plan Projected End</label>
@@ -429,6 +450,72 @@
</tags>
<state pattern="%d" readOnly="true"/>
</channel-type>
<channel-type id="loadpoint-charge-current-l1">
<item-type unitHint="A">Number:ElectricCurrent</item-type>
<label>Charge Current 1</label>
<description>Actual charge current of charger phase L1</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Current</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="loadpoint-charge-current-l2">
<item-type unitHint="A">Number:ElectricCurrent</item-type>
<label>Charge Current 2</label>
<description>Actual charge current of charger phase L2</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Current</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="loadpoint-charge-current-l3">
<item-type unitHint="A">Number:ElectricCurrent</item-type>
<label>Charge Current 3</label>
<description>Actual charge current of charger phase L3</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Current</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="loadpoint-charge-voltage-l1">
<item-type unitHint="V">Number:ElectricPotential</item-type>
<label>Charge Voltage 1</label>
<description>Actual charge voltage of charger phase L1</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="loadpoint-charge-voltage-l2">
<item-type unitHint="V">Number:ElectricPotential</item-type>
<label>Charge Voltage 2</label>
<description>Actual charge voltage of charger phase L2</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="loadpoint-charge-voltage-l3">
<item-type unitHint="V">Number:ElectricPotential</item-type>
<label>Charge Voltage 3</label>
<description>Actual charge voltage of charger phase L3</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="loadpoint-phases-active">
<item-type>Number</item-type>
<label>Active Phases</label>
@@ -10,6 +10,12 @@
<label>Photovoltaic</label>
<description>A photovoltaic system configured in your evcc instance</description>
<config-description>
<parameter name="index" type="integer">
<label>Index</label>
<description>The index of the photovoltaic system in your evcc setup (starting with 0)</description>
</parameter>
</config-description>
</thing-type>
<!-- Already declared in site.xml maybe evcc is going to change it in the future -->
<channel-type id="pv-power">
@@ -195,6 +195,83 @@
<description>Indicates whether green power is available for loadpoints</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="site-grid-current-l1">
<item-type unitHint="A">Number:ElectricCurrent</item-type>
<label>Grid Current 1</label>
<description>Grid phase 1 current</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Current</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="site-grid-current-l2">
<item-type unitHint="A">Number:ElectricCurrent</item-type>
<label>Grid Current 2</label>
<description>Grid phase 2 current</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Current</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="site-grid-current-l3">
<item-type unitHint="A">Number:ElectricCurrent</item-type>
<label>Grid Current 3</label>
<description>Grid phase 3 current</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Current</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="site-grid-voltage-l1">
<item-type unitHint="V">Number:ElectricPotential</item-type>
<label>Charge Voltage 1</label>
<description>Actual charge voltage of charger phase L1</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="site-grid-voltage-l2">
<item-type unitHint="V">Number:ElectricPotential</item-type>
<label>Charge Voltage 2</label>
<description>Actual charge voltage of charger phase L2</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="site-grid-voltage-l3">
<item-type unitHint="V">Number:ElectricPotential</item-type>
<label>Charge Voltage 3</label>
<description>Actual charge voltage of charger phase L3</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="site-grid-energy">
<item-type unitHint="kWh">Number:Energy</item-type>
<label>Grid Energy</label>
<description>Consumed energy from the grid</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Energy</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="site-grid-power">
<item-type unitHint="W">Number:Power</item-type>
<label>Grid Power</label>
@@ -237,14 +314,14 @@
<state min="0" step="0.1" max="100" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="site-interval">
<item-type>Number</item-type>
<item-type unitHint="s">Number:Time</item-type>
<label>Interval</label>
<description>Refresh interval configured for evcc</description>
<category>Settings</category>
<state pattern="%d" readOnly="true"/>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="site-pv-energy">
<item-type unitHint="Wh">Number:Energy</item-type>
<item-type unitHint="kWh">Number:Energy</item-type>
<label>PV Energy</label>
<description>Accumulated energy provided through photovoltaic systems</description>
<category>Energy</category>
@@ -42,7 +42,7 @@
<channel-type id="avg-co2-type">
<item-type unitHint="g/kWh">Number:EmissionIntensity</item-type>
<label>Average C02</label>
<label>Average CO2</label>
<tags>
<tag>Calculation</tag>
<tag>Info</tag>
@@ -10,6 +10,12 @@
<label>Vehicle</label>
<description>A vehicle configured in your evcc instance</description>
<representation-property>id</representation-property>
<config-description>
<parameter name="id" type="text">
<label>ID</label>
<description>The ID of the vehicle in your evcc setup (database ID)</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="vehicle-capacity">
<item-type unitHint="kWh">Number:Energy</item-type>
@@ -0,0 +1,115 @@
/*
* 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.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;
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;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* 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<T extends EvccBaseThingHandler> {
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;
protected static JsonObject exampleResponse = new JsonObject();
protected static JsonObject verifyObject = new JsonObject();
/**
* Implement this to provide a handler instance for testing.
*/
protected abstract T createHandler();
@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<>());
Configuration configuration = mock(Configuration.class);
when(configuration.get("index")).thenReturn("0");
when(configuration.get("id")).thenReturn("vehicle_1");
when(thing.getConfiguration()).thenReturn(configuration);
}
@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);
}
}
}
@@ -0,0 +1,96 @@
/*
* 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 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
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) {
}
@Override
public void logUnknownChannelXmlAsync(String key, String itemType) {
logUnknownChannelXmlCalled = true;
super.logUnknownChannelXmlAsync(key, itemType);
}
}
@@ -0,0 +1,299 @@
/*
* 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 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.*;
import static org.openhab.binding.evcc.internal.EvccBindingConstants.NUMBER_ENERGY;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
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.openhab.core.config.core.Configuration;
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.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.JsonObject;
import com.google.gson.JsonPrimitive;
/**
* The {@link EvccBaseThingHandlerTest} is responsible for testing the EvccBaseThingHandler implementation
*
* @author Marcel Goerentz - Initial contribution
*/
@NonNullByDefault
public class EvccBaseThingHandlerTest {
@SuppressWarnings("null")
private final Thing thing = mock(Thing.class);
@SuppressWarnings("null")
private final ChannelTypeRegistry channelTypeRegistry = mock(ChannelTypeRegistry.class);
private BaseThingHandlerTestClass handler = new BaseThingHandlerTestClass(thing, channelTypeRegistry);
@SuppressWarnings("null")
@BeforeEach
public void setUp() {
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(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
class UpdateStatesFromApiResponseTests {
@Test
public void updateFromEvccStateNotInitializedDoesNothing() {
handler.isInitialized = false;
JsonObject state = new JsonObject();
handler.updateStatesFromApiResponse(state);
assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled);
assertFalse(handler.createChannelCalled);
assertFalse(handler.updateThingCalled);
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.createChannelCalled);
assertFalse(handler.updateThingCalled);
// Status should not be updated for empty state
assertFalse(handler.updateStatusCalled);
assertEquals(ThingStatus.UNKNOWN, handler.lastUpdatedStatus);
}
@SuppressWarnings("null")
@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);
ChannelType mockChannelType = mock(ChannelType.class);
when(mockChannelType.getItemType()).thenReturn(NUMBER_ENERGY);
when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType);
handler.updateStatesFromApiResponse(state);
assertTrue(handler.prepareApiResponseForChannelStateUpdateCalled);
assertTrue(handler.createChannelCalled);
assertTrue(handler.updateThingCalled);
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);
assertFalse(handler.updateThingCalled); // Should not update thing if channel exists
assertTrue(handler.updateStatusCalled);
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.updateThingCalled);
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.updateThingCalled);
assertTrue(handler.updateStatusCalled);
assertEquals(ThingStatus.ONLINE, handler.lastUpdatedStatus);
}
}
@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));
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);
}
@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));
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);
}
@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());
handler.handleCommand(channelUID, command);
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);
}
}
@Test
public void testCreateChannelWithUnknownItemType() {
JsonElement value = new JsonPrimitive(5.5);
@SuppressWarnings("null")
ThingBuilder builder = mock(ThingBuilder.class);
@SuppressWarnings("null")
ChannelType mockChannelType = mock(ChannelType.class);
when(mockChannelType.getItemType()).thenReturn("Unknown");
when(channelTypeRegistry.getChannelType(any())).thenReturn(mockChannelType);
handler.createChannel("capacity", value);
assertTrue(handler.createChannelCalled);
verify(builder, never()).withChannel(any());
}
@Nested
class GetThingKeyTests {
@Test
public void getThingKeyWithBatteryTypeAndSpecialKey() {
handler.type = "battery";
String key = "soc";
String result = handler.getThingKey(key);
assertEquals("battery-soc", result);
}
@Test
public void getThingKeyWithHeatingType() {
handler.type = "loadpoint";
String key = "capacity";
String result = handler.getThingKey(key);
assertEquals("loadpoint-capacity", result);
}
@Test
public void getThingKeyWithDefaultType() {
handler.type = "loadpoint";
String key = "someKey";
String result = handler.getThingKey(key);
assertEquals("loadpoint-some-key", result);
}
}
@SuppressWarnings("null")
@Test
void logUnknownChannelXmlAsyncIsCalledAsynchronously() {
CompletableFuture<Boolean> 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");
}
}
}
@@ -0,0 +1,129 @@
/*
* 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.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;
/**
* The {@link EvccBatteryHandlerTest} is responsible for testing the EvccBatteryHandler implementation
*
* @author Marcel Goerentz - Initial contribution
*/
@NonNullByDefault
public class EvccBatteryHandlerTest extends AbstractThingHandlerTestClass<EvccBatteryHandler> {
private static JsonObject batteryState = new JsonObject();
@Override
protected EvccBatteryHandler createHandler() {
return new EvccBatteryHandler(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) {
}
@Override
protected void updateState(ChannelUID channelUID, State state) {
}
};
}
@SuppressWarnings("null")
@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<>());
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;
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")
@Test
public void testInitializeWithBridgeHandlerWithValidState() {
handler.initialize();
assertSame(ThingStatus.ONLINE, lastThingStatus);
}
@SuppressWarnings("null")
@Test
public void testPrepareApiResponseForChannelStateUpdateIsInitialized() {
handler.isInitialized = true;
handler.prepareApiResponseForChannelStateUpdate(exampleResponse);
assertSame(ThingStatus.ONLINE, lastThingStatus);
}
@SuppressWarnings("null")
@Test
public void testGetStateFromCachedState() {
JsonObject result = handler.getStateFromCachedState(exampleResponse);
assertSame(batteryState, result);
}
}
@@ -0,0 +1,147 @@
/*
* 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.*;
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.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;
/**
* The {@link EvccHeatingHandlerTest} is responsible for testing the EvccHeatingHandler implementation
*
* @author Marcel Goerentz - Initial contribution
*/
@NonNullByDefault
public class EvccHeatingHandlerTest extends AbstractThingHandlerTestClass<EvccHeatingHandler> {
private JsonObject heatingObject = 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) {
}
@Override
protected void updateState(ChannelUID channelUID, State state) {
}
};
}
@SuppressWarnings("null")
@BeforeEach
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;
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() {
handler.initialize();
assertSame(ThingStatus.ONLINE, lastThingStatus);
}
@SuppressWarnings("null")
@Test
public void testPrepareApiResponseForChannelStateUpdateIsInitialized() {
handler.isInitialized = true;
handler.prepareApiResponseForChannelStateUpdate(exampleResponse);
assertSame(ThingStatus.ONLINE, lastThingStatus);
}
@SuppressWarnings("null")
@Test
public void testPrepareApiResponseForChannelStateUpdateIsNotInitialized() {
handler.prepareApiResponseForChannelStateUpdate(exampleResponse);
assertSame(ThingStatus.UNKNOWN, lastThingStatus);
}
@SuppressWarnings("null")
@Test
public void testJsonGetsModifiedCorrectly() {
handler.prepareApiResponseForChannelStateUpdate(exampleResponse);
assertEquals(heatingObject, exampleResponse.getAsJsonArray("loadpoints").get(0));
}
@SuppressWarnings("null")
@Test
public void testGetStateFromCachedState() {
JsonObject result = handler.getStateFromCachedState(exampleResponse);
assertSame(exampleResponse.getAsJsonArray("loadpoints").get(0), result);
}
}
@@ -0,0 +1,176 @@
/*
* 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.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;
/**
* The {@link EvccLoadpointHandlerTest} is responsible for testing the EvccLoadpointHandler implementation
*
* @author Marcel Goerentz - Initial contribution
*/
@NonNullByDefault
public class EvccLoadpointHandlerTest extends AbstractThingHandlerTestClass<EvccLoadpointHandler> {
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() {
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) {
}
@Override
protected void updateState(ChannelUID channelUID, State state) {
}
};
}
@BeforeEach
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);
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_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 = exampleResponse.getAsJsonArray("loadpoints");
loadpointArray.set(0, testObject);
modifiedTestState.add("loadpoints", loadpointArray);
}
@SuppressWarnings("null")
@Test
public void testInitializeWithBridgeHandlerWithValidState() {
EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class);
handler.bridgeHandler = bridgeHandler;
when(bridgeHandler.getCachedEvccState()).thenReturn(exampleResponse);
handler.initialize();
assertSame(ThingStatus.ONLINE, lastThingStatus);
}
@SuppressWarnings("null")
@Test
public void testPrepareApiResponseForChannelStateUpdateIsInitialized() {
handler.bridgeHandler = mock(EvccBridgeHandler.class);
handler.isInitialized = true;
handler.prepareApiResponseForChannelStateUpdate(exampleResponse);
assertSame(ThingStatus.ONLINE, lastThingStatus);
}
@SuppressWarnings("null")
@Test
public void testPrepareApiResponseForChannelStateUpdateIsNotInitialized() {
handler.bridgeHandler = mock(EvccBridgeHandler.class);
handler.prepareApiResponseForChannelStateUpdate(exampleResponse);
assertSame(ThingStatus.UNKNOWN, lastThingStatus);
}
@SuppressWarnings("null")
@Test
public void testJsonGetsModifiedCorrectly() {
handler.prepareApiResponseForChannelStateUpdate(exampleResponse);
assertEquals(modifiedVerifyObject, modifiedTestState.getAsJsonArray("loadpoints").get(0));
}
@SuppressWarnings("null")
@Test
public void testGetStateFromCachedState() {
JsonObject result = handler.getStateFromCachedState(exampleResponse);
assertSame(exampleResponse.getAsJsonArray("loadpoints").get(0), result);
}
}
@@ -0,0 +1,141 @@
/*
* 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.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;
/**
* The {@link EvccSiteHandlerTest} is responsible for testing the EvccPvHandler implementation
*
* @author Marcel Goerentz - Initial contribution
*/
@NonNullByDefault
public class EvccPvHandlerTest extends AbstractThingHandlerTestClass<EvccPvHandler> {
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) {
}
@Override
protected void updateState(ChannelUID channelUID, State state) {
}
};
}
@BeforeEach
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);
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);
}
}
@@ -0,0 +1,167 @@
/*
* 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.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;
/**
* The {@link EvccSiteHandlerTest} is responsible for testing the EvccSiteHandler implementation
*
* @author Marcel Goerentz - Initial contribution
*/
@NonNullByDefault
public class EvccSiteHandlerTest extends AbstractThingHandlerTestClass<EvccSiteHandler> {
private final JsonObject gridConfigured = new JsonObject();
private final JsonObject modifiedVerifyObject = verifyObject.deepCopy();
@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) {
}
@Override
protected void updateState(ChannelUID channelUID, State state) {
}
};
}
@BeforeEach
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<>());
handler = spy(createHandler());
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);
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);
}
@SuppressWarnings("null")
@Test
public void testInitializeWithBridgeHandlerWithValidState() {
EvccBridgeHandler bridgeHandler = mock(EvccBridgeHandler.class);
handler.bridgeHandler = bridgeHandler;
when(bridgeHandler.getCachedEvccState()).thenReturn(exampleResponse);
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(exampleResponse);
assertSame(ThingStatus.ONLINE, lastThingStatus);
}
@Test
public void handlerIsNotInitialized() {
handler.bridgeHandler = mock(EvccBridgeHandler.class);
handler.prepareApiResponseForChannelStateUpdate(exampleResponse);
assertSame(ThingStatus.UNKNOWN, lastThingStatus);
}
@Test
public void stateContainsGridConfigured() {
handler.bridgeHandler = mock(EvccBridgeHandler.class);
exampleResponse.addProperty("gridConfigured", true);
exampleResponse.add("grid", gridConfigured);
handler.prepareApiResponseForChannelStateUpdate(exampleResponse);
assertEquals(modifiedVerifyObject, exampleResponse);
}
}
@SuppressWarnings("null")
@Test
public void testGetStateFromCachedState() {
JsonObject result = handler.getStateFromCachedState(exampleResponse);
assertSame(exampleResponse, result);
}
}
@@ -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<EvccStatisticsHandler> {
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);
}
}
}
@@ -0,0 +1,100 @@
/*
* 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.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
*
* @author Marcel Goerentz - Initial contribution
*/
@NonNullByDefault
public class EvccVehicleHandlerTest extends AbstractThingHandlerTestClass<EvccVehicleHandler> {
@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) {
}
@Override
protected void updateState(ChannelUID channelUID, State state) {
}
};
}
@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<>());
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;
when(bridgeHandler.getCachedEvccState()).thenReturn(exampleResponse);
}
@SuppressWarnings("null")
@Test
public void testInitializeWithBridgeHandlerWithValidState() {
handler.initialize();
assertSame(ThingStatus.ONLINE, lastThingStatus);
}
}