[fronius] Fix invalid credentials lead to unexpected exception (#18130)

With the changes from this PR, the status code is now properly read and for 401, a meaningful warnings is logged instead.

Signed-off-by: Florian Hotze <dev@florianhotze.com>
pull/18131/head
Florian Hotze 2025-01-19 17:05:38 +01:00 committed by GitHub
parent 5fc40cc353
commit 8f77e16e18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 121 additions and 25 deletions

View File

@ -77,8 +77,9 @@ public class FroniusBatteryControl {
*
* @return the time of use settings
* @throws FroniusCommunicationException if an error occurs during communication with the inverter
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
*/
private TimeOfUseRecords getTimeOfUse() throws FroniusCommunicationException {
private TimeOfUseRecords getTimeOfUse() throws FroniusCommunicationException, FroniusUnauthorizedException {
// Login and get the auth header for the next request
String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.GET,
timeOfUseUri.getPath(), API_TIMEOUT);
@ -107,8 +108,10 @@ public class FroniusBatteryControl {
*
* @param records the time of use settings
* @throws FroniusCommunicationException if an error occurs during communication with the inverter
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
*/
private void setTimeOfUse(TimeOfUseRecords records) throws FroniusCommunicationException {
private void setTimeOfUse(TimeOfUseRecords records)
throws FroniusCommunicationException, FroniusUnauthorizedException {
// Login and get the auth header for the next request
String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.POST,
timeOfUseUri.getPath(), API_TIMEOUT);
@ -127,8 +130,9 @@ public class FroniusBatteryControl {
* inverter.
*
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
*/
public void reset() throws FroniusCommunicationException {
public void reset() throws FroniusCommunicationException, FroniusUnauthorizedException {
setTimeOfUse(new TimeOfUseRecords(new TimeOfUseRecord[0]));
}
@ -136,8 +140,9 @@ public class FroniusBatteryControl {
* Holds the battery charge right now, i.e. prevents the battery from discharging.
*
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
*/
public void holdBatteryCharge() throws FroniusCommunicationException {
public void holdBatteryCharge() throws FroniusCommunicationException, FroniusUnauthorizedException {
reset();
addHoldBatteryChargeSchedule(BEGIN_OF_DAY, END_OF_DAY);
}
@ -149,8 +154,10 @@ public class FroniusBatteryControl {
* @param from start time of the hold charge period
* @param until end time of the hold charge period
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
*/
public void addHoldBatteryChargeSchedule(LocalTime from, LocalTime until) throws FroniusCommunicationException {
public void addHoldBatteryChargeSchedule(LocalTime from, LocalTime until)
throws FroniusCommunicationException, FroniusUnauthorizedException {
TimeOfUseRecord[] currentTimeOfUse = getTimeOfUse().records();
TimeOfUseRecord[] timeOfUse = new TimeOfUseRecord[currentTimeOfUse.length + 1];
System.arraycopy(currentTimeOfUse, 0, timeOfUse, 0, currentTimeOfUse.length);
@ -166,8 +173,10 @@ public class FroniusBatteryControl {
*
* @param power the power to charge the battery with
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
*/
public void forceBatteryCharging(QuantityType<Power> power) throws FroniusCommunicationException {
public void forceBatteryCharging(QuantityType<Power> power)
throws FroniusCommunicationException, FroniusUnauthorizedException {
reset();
addForcedBatteryChargingSchedule(BEGIN_OF_DAY, END_OF_DAY, power);
}
@ -179,9 +188,10 @@ public class FroniusBatteryControl {
* @param until end time of the forced charge period
* @param power the power to charge the battery with
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
*/
public void addForcedBatteryChargingSchedule(LocalTime from, LocalTime until, QuantityType<Power> power)
throws FroniusCommunicationException {
throws FroniusCommunicationException, FroniusUnauthorizedException {
TimeOfUseRecords currentTimeOfUse = getTimeOfUse();
TimeOfUseRecord[] timeOfUse = new TimeOfUseRecord[currentTimeOfUse.records().length + 1];
System.arraycopy(currentTimeOfUse.records(), 0, timeOfUse, 0, currentTimeOfUse.records().length);

View File

@ -19,14 +19,11 @@ import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.http.HttpHeader;
@ -69,10 +66,10 @@ public class FroniusConfigAuthUtil {
throws IOException {
LOGGER.debug("Sending login request to get authentication challenge");
CountDownLatch latch = new CountDownLatch(1);
Request initialRequest = httpClient.newRequest(loginUri).timeout(timeout, TimeUnit.MILLISECONDS);
XWwwAuthenticateHeaderListener XWwwAuthenticateHeaderListener = new XWwwAuthenticateHeaderListener(latch);
initialRequest.onResponseHeaders(XWwwAuthenticateHeaderListener);
initialRequest.send(result -> latch.countDown());
Request request = httpClient.newRequest(loginUri).timeout(timeout, TimeUnit.MILLISECONDS);
XWwwAuthenticateHeaderListener xWwwAuthenticateHeaderListener = new XWwwAuthenticateHeaderListener(latch);
request.onResponseHeaders(xWwwAuthenticateHeaderListener);
request.send(result -> latch.countDown());
// Wait for the request to complete
try {
latch.await();
@ -80,7 +77,7 @@ public class FroniusConfigAuthUtil {
throw new RuntimeException(ie);
}
String authHeader = XWwwAuthenticateHeaderListener.getAuthHeader();
String authHeader = xWwwAuthenticateHeaderListener.getAuthHeader();
if (authHeader == null) {
throw new IOException("No authentication header found in login response");
}
@ -161,21 +158,40 @@ public class FroniusConfigAuthUtil {
* @param authHeader the authentication header to use for the login request
* @throws InterruptedException when the request is interrupted
* @throws FroniusCommunicationException when the login request failed
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
*/
private static void performLoginRequest(HttpClient httpClient, URI loginUri, String authHeader, int timeout)
throws InterruptedException, FroniusCommunicationException {
Request loginRequest = httpClient.newRequest(loginUri).header(HttpHeader.AUTHORIZATION, authHeader)
.timeout(timeout, TimeUnit.MILLISECONDS);
ContentResponse loginResponse;
throws InterruptedException, FroniusCommunicationException, FroniusUnauthorizedException {
CountDownLatch latch = new CountDownLatch(1);
Request request = httpClient.newRequest(loginUri).header(HttpHeader.AUTHORIZATION, authHeader).timeout(timeout,
TimeUnit.MILLISECONDS);
StatusListener statusListener = new StatusListener(latch);
request.onResponseBegin(statusListener);
Integer status;
try {
loginResponse = loginRequest.send();
if (loginResponse.getStatus() != 200) {
throw new FroniusCommunicationException(
"Failed to send login request, status code: " + loginResponse.getStatus());
request.send(result -> latch.countDown());
// Wait for the request to complete
try {
latch.await();
} catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
} catch (TimeoutException | ExecutionException e) {
status = statusListener.getStatus();
if (status == null) {
throw new FroniusCommunicationException("Failed to send login request: No status code received.");
}
} catch (IOException e) {
throw new FroniusCommunicationException("Failed to send login request", e);
}
if (status == 401) {
throw new FroniusUnauthorizedException(
"Failed to send login request, status code: 401 Unauthorized. Please check your credentials.");
}
if (status != 200) {
throw new FroniusCommunicationException("Failed to send login request, status code: " + status);
}
}
/**
@ -191,9 +207,11 @@ public class FroniusConfigAuthUtil {
* @param timeout the timeout in milliseconds for the login requests
* @return the authentication header for the next request
* @throws FroniusCommunicationException when the login failed or interrupted
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
*/
public static synchronized String login(HttpClient httpClient, URI baseUri, String username, String password,
HttpMethod method, String relativeUrl, int timeout) throws FroniusCommunicationException {
HttpMethod method, String relativeUrl, int timeout)
throws FroniusCommunicationException, FroniusUnauthorizedException {
// Perform request to get authentication parameters
LOGGER.debug("Getting authentication parameters");
URI loginUri = baseUri.resolve(URI.create(LOGIN_ENDPOINT + "?user=" + username));
@ -246,6 +264,8 @@ public class FroniusConfigAuthUtil {
Thread.sleep(500 * attemptCount);
attemptCount++;
lastException = e;
} catch (FroniusUnauthorizedException e) {
throw e;
}
if (attemptCount >= 3) {
@ -269,6 +289,9 @@ public class FroniusConfigAuthUtil {
/**
* Listener to extract the X-Www-Authenticate header from the response of a {@link Request}.
* Required to mitigate {@link org.eclipse.jetty.client.HttpResponseException}: HTTP protocol violation:
* Authentication challenge without WWW-Authenticate header being thrown due to Fronius non-standard authentication
* header.
*/
private static class XWwwAuthenticateHeaderListener extends Response.Listener.Adapter {
private final CountDownLatch latch;
@ -288,4 +311,30 @@ public class FroniusConfigAuthUtil {
return authHeader;
}
}
/**
* Listener to extract the HTTP status code from the response of a {@link Request} on response begin.
* Required to mitigate {@link org.eclipse.jetty.client.HttpResponseException}: HTTP protocol violation:
* Authentication challenge without WWW-Authenticate header being thrown due to Fronius non-standard authentication
* header.
*/
private static class StatusListener extends Response.Listener.Adapter {
private final CountDownLatch latch;
private @Nullable Integer status;
public StatusListener(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void onBegin(Response response) {
this.status = response.getStatus();
latch.countDown();
super.onBegin(response);
}
public @Nullable Integer getStatus() {
return status;
}
}
}

View File

@ -0,0 +1,24 @@
/*
* 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.fronius.internal.api;
/**
* Exception for 401 response from the Fronius controller.
*
* @author Florian Hotze - Initial contribution
*/
public class FroniusUnauthorizedException extends Exception {
public FroniusUnauthorizedException(String message) {
super(message);
}
}

View File

@ -29,6 +29,7 @@ import org.openhab.binding.fronius.internal.FroniusBridgeConfiguration;
import org.openhab.binding.fronius.internal.action.FroniusSymoInverterActions;
import org.openhab.binding.fronius.internal.api.FroniusBatteryControl;
import org.openhab.binding.fronius.internal.api.FroniusCommunicationException;
import org.openhab.binding.fronius.internal.api.FroniusUnauthorizedException;
import org.openhab.binding.fronius.internal.api.dto.ValueUnit;
import org.openhab.binding.fronius.internal.api.dto.inverter.InverterDeviceStatus;
import org.openhab.binding.fronius.internal.api.dto.inverter.InverterRealtimeBody;
@ -115,6 +116,8 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
return true;
} catch (FroniusCommunicationException e) {
logger.warn("Failed to reset battery control", e);
} catch (FroniusUnauthorizedException e) {
logger.warn("Failed to reset battery control: Invalid username or password");
}
}
return false;
@ -128,6 +131,8 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
return true;
} catch (FroniusCommunicationException e) {
logger.warn("Failed to set battery control to hold battery charge", e);
} catch (FroniusUnauthorizedException e) {
logger.warn("Failed to set battery control to hold battery charge: Invalid username or password");
}
}
return false;
@ -141,6 +146,9 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
return true;
} catch (FroniusCommunicationException e) {
logger.warn("Failed to add hold battery charge schedule to battery control", e);
} catch (FroniusUnauthorizedException e) {
logger.warn(
"Failed to add hold battery charge schedule to battery control: Invalid username or password");
}
}
return false;
@ -154,6 +162,8 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
return true;
} catch (FroniusCommunicationException e) {
logger.warn("Failed to set battery control to force battery charge", e);
} catch (FroniusUnauthorizedException e) {
logger.warn("Failed to set battery control to force battery charge: Invalid username or password");
}
}
return false;
@ -167,6 +177,9 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
return true;
} catch (FroniusCommunicationException e) {
logger.warn("Failed to add forced battery charge schedule to battery control", e);
} catch (FroniusUnauthorizedException e) {
logger.warn(
"Failed to add forced battery charge schedule to battery control: Invalid username or password");
}
}
return false;