[easee] Fix offline detection and recovery on connection loss (#20891)

* [easee] fix offline/recovery handling on connection loss

Signed-off-by: Alexander Friese <af944580@googlemail.com>
This commit is contained in:
Alexander Friese
2026-06-05 16:10:08 +02:00
committed by GitHub
parent 96613bc338
commit 1019a07a20
6 changed files with 46 additions and 20 deletions
@@ -20,8 +20,10 @@ import java.time.format.DateTimeParseException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpStatus.Code;
import org.openhab.binding.easee.internal.model.ConfigurationException;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -120,6 +122,26 @@ public final class Utils {
return (json == null || json instanceof JsonNull) ? null : json.getAsBoolean();
}
/**
* maps an HTTP status code of a failed request to the matching {@link ThingStatusDetail}. Only genuine
* authentication/configuration problems (e.g. bad credentials) are reported as configuration error, everything
* else (timeouts, gateway errors, server errors, ...) is treated as a communication error.
*
* @param httpCode the HTTP status code of the failed request
* @return the matching ThingStatusDetail
*/
public static ThingStatusDetail getStatusDetailFromHttpCode(Code httpCode) {
switch (httpCode) {
case BAD_REQUEST:
case UNAUTHORIZED:
case FORBIDDEN: // e.g. wrong site id
case NOT_FOUND: // e.g. wrong user name
return ThingStatusDetail.CONFIGURATION_ERROR;
default:
return ThingStatusDetail.COMMUNICATION_ERROR;
}
}
/**
* retrieves typeID of a channel.
*
@@ -72,11 +72,14 @@ public class Login extends AbstractCommand {
@Override
public void onComplete(@Nullable Result result) {
String json = getContentAsString(StandardCharsets.UTF_8);
JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
if (jsonObject != null) {
processResult(jsonObject);
// on connectivity failures (e.g. timeout) the body is empty or not parseable. We still need to process the
// result so that the authentication state is reset and the status is updated accordingly.
JsonObject jsonObject = transform(json, JsonObject.class);
if (jsonObject == null) {
jsonObject = new JsonObject();
}
processResult(jsonObject);
}
@Override
@@ -89,6 +89,10 @@ public class SiteState extends AbstractCommand {
processChargerStateData(chargerData.getAsJsonObject());
}
}
// a successful site poll is the health signal of the bridge, so update the bridge status as well.
// Otherwise the bridge would only recover on the next token refresh/login (which can be up to an hour
// later) while the chargers already went back online via setOnline().
processResult(jsonObject);
}
}
@@ -135,11 +135,6 @@ public class WebInterface implements AtomicReferenceTrait {
}
switch (status.getHttpCode()) {
case BAD_REQUEST:
bridgeStatusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
msg);
setAuthenticated(false);
break;
case OK:
String accessToken = Utils.getAsString(jsonObject, JSON_KEY_AUTH_ACCESS_TOKEN);
String refreshToken = Utils.getAsString(jsonObject, JSON_KEY_AUTH_REFRESH_TOKEN);
@@ -160,8 +155,8 @@ public class WebInterface implements AtomicReferenceTrait {
break;
}
default:
bridgeStatusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
msg);
bridgeStatusHandler.updateStatusInfo(ThingStatus.OFFLINE,
Utils.getStatusDetailFromHttpCode(status.getHttpCode()), msg);
setAuthenticated(false);
}
}
@@ -190,18 +185,20 @@ public class WebInterface implements AtomicReferenceTrait {
@Override
public void run() {
logger.debug("run queued commands, queue size is {}", commandQueue.size());
if (!isAuthenticated()) {
authenticate();
} else {
refreshAccessToken();
// catch all exceptions here: the executor is scheduled with a fixed delay, an uncaught exception would
// silently cancel the job and the binding would never recover.
try {
if (!isAuthenticated()) {
authenticate();
} else {
refreshAccessToken();
if (isAuthenticated() && !commandQueue.isEmpty()) {
try {
if (isAuthenticated() && !commandQueue.isEmpty()) {
executeCommand();
} catch (Exception ex) {
logger.warn("command execution ended with exception:", ex);
}
}
} catch (Exception ex) {
logger.warn("command execution ended with exception:", ex);
}
}
@@ -104,7 +104,7 @@ public abstract class EaseeBaseThingHandler extends BaseThingHandler
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
break;
default:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
updateStatus(ThingStatus.OFFLINE, Utils.getStatusDetailFromHttpCode(status.getHttpCode()), msg);
}
}
@@ -160,7 +160,7 @@ public class EaseeSiteHandler extends BaseBridgeHandler implements EaseeBridgeHa
super.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
break;
default:
super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
super.updateStatus(ThingStatus.OFFLINE, Utils.getStatusDetailFromHttpCode(status.getHttpCode()), msg);
}
}