[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 * @return the time of use settings
* @throws FroniusCommunicationException if an error occurs during communication with the inverter * @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 // Login and get the auth header for the next request
String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.GET, String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.GET,
timeOfUseUri.getPath(), API_TIMEOUT); timeOfUseUri.getPath(), API_TIMEOUT);
@ -107,8 +108,10 @@ public class FroniusBatteryControl {
* *
* @param records the time of use settings * @param records the time of use settings
* @throws FroniusCommunicationException if an error occurs during communication with the inverter * @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 // Login and get the auth header for the next request
String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.POST, String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.POST,
timeOfUseUri.getPath(), API_TIMEOUT); timeOfUseUri.getPath(), API_TIMEOUT);
@ -127,8 +130,9 @@ public class FroniusBatteryControl {
* inverter. * inverter.
* *
* @throws FroniusCommunicationException when an error occurs during communication with the 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])); 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. * 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 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(); reset();
addHoldBatteryChargeSchedule(BEGIN_OF_DAY, END_OF_DAY); addHoldBatteryChargeSchedule(BEGIN_OF_DAY, END_OF_DAY);
} }
@ -149,8 +154,10 @@ public class FroniusBatteryControl {
* @param from start time of the hold charge period * @param from start time of the hold charge period
* @param until end 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 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[] currentTimeOfUse = getTimeOfUse().records();
TimeOfUseRecord[] timeOfUse = new TimeOfUseRecord[currentTimeOfUse.length + 1]; TimeOfUseRecord[] timeOfUse = new TimeOfUseRecord[currentTimeOfUse.length + 1];
System.arraycopy(currentTimeOfUse, 0, timeOfUse, 0, currentTimeOfUse.length); System.arraycopy(currentTimeOfUse, 0, timeOfUse, 0, currentTimeOfUse.length);
@ -166,8 +173,10 @@ public class FroniusBatteryControl {
* *
* @param power the power to charge the battery with * @param power the power to charge the battery with
* @throws FroniusCommunicationException when an error occurs during communication with the inverter * @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(); reset();
addForcedBatteryChargingSchedule(BEGIN_OF_DAY, END_OF_DAY, power); 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 until end time of the forced charge period
* @param power the power to charge the battery with * @param power the power to charge the battery with
* @throws FroniusCommunicationException when an error occurs during communication with the inverter * @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) public void addForcedBatteryChargingSchedule(LocalTime from, LocalTime until, QuantityType<Power> power)
throws FroniusCommunicationException { throws FroniusCommunicationException, FroniusUnauthorizedException {
TimeOfUseRecords currentTimeOfUse = getTimeOfUse(); TimeOfUseRecords currentTimeOfUse = getTimeOfUse();
TimeOfUseRecord[] timeOfUse = new TimeOfUseRecord[currentTimeOfUse.records().length + 1]; TimeOfUseRecord[] timeOfUse = new TimeOfUseRecord[currentTimeOfUse.records().length + 1];
System.arraycopy(currentTimeOfUse.records(), 0, timeOfUse, 0, currentTimeOfUse.records().length); 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.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; 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.Request;
import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
@ -69,10 +66,10 @@ public class FroniusConfigAuthUtil {
throws IOException { throws IOException {
LOGGER.debug("Sending login request to get authentication challenge"); LOGGER.debug("Sending login request to get authentication challenge");
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
Request initialRequest = httpClient.newRequest(loginUri).timeout(timeout, TimeUnit.MILLISECONDS); Request request = httpClient.newRequest(loginUri).timeout(timeout, TimeUnit.MILLISECONDS);
XWwwAuthenticateHeaderListener XWwwAuthenticateHeaderListener = new XWwwAuthenticateHeaderListener(latch); XWwwAuthenticateHeaderListener xWwwAuthenticateHeaderListener = new XWwwAuthenticateHeaderListener(latch);
initialRequest.onResponseHeaders(XWwwAuthenticateHeaderListener); request.onResponseHeaders(xWwwAuthenticateHeaderListener);
initialRequest.send(result -> latch.countDown()); request.send(result -> latch.countDown());
// Wait for the request to complete // Wait for the request to complete
try { try {
latch.await(); latch.await();
@ -80,7 +77,7 @@ public class FroniusConfigAuthUtil {
throw new RuntimeException(ie); throw new RuntimeException(ie);
} }
String authHeader = XWwwAuthenticateHeaderListener.getAuthHeader(); String authHeader = xWwwAuthenticateHeaderListener.getAuthHeader();
if (authHeader == null) { if (authHeader == null) {
throw new IOException("No authentication header found in login response"); 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 * @param authHeader the authentication header to use for the login request
* @throws InterruptedException when the request is interrupted * @throws InterruptedException when the request is interrupted
* @throws FroniusCommunicationException when the login request failed * @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) private static void performLoginRequest(HttpClient httpClient, URI loginUri, String authHeader, int timeout)
throws InterruptedException, FroniusCommunicationException { throws InterruptedException, FroniusCommunicationException, FroniusUnauthorizedException {
Request loginRequest = httpClient.newRequest(loginUri).header(HttpHeader.AUTHORIZATION, authHeader) CountDownLatch latch = new CountDownLatch(1);
.timeout(timeout, TimeUnit.MILLISECONDS); Request request = httpClient.newRequest(loginUri).header(HttpHeader.AUTHORIZATION, authHeader).timeout(timeout,
ContentResponse loginResponse; TimeUnit.MILLISECONDS);
StatusListener statusListener = new StatusListener(latch);
request.onResponseBegin(statusListener);
Integer status;
try { try {
loginResponse = loginRequest.send(); request.send(result -> latch.countDown());
if (loginResponse.getStatus() != 200) { // Wait for the request to complete
throw new FroniusCommunicationException( try {
"Failed to send login request, status code: " + loginResponse.getStatus()); 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); 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 * @param timeout the timeout in milliseconds for the login requests
* @return the authentication header for the next request * @return the authentication header for the next request
* @throws FroniusCommunicationException when the login failed or interrupted * @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, 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 // Perform request to get authentication parameters
LOGGER.debug("Getting authentication parameters"); LOGGER.debug("Getting authentication parameters");
URI loginUri = baseUri.resolve(URI.create(LOGIN_ENDPOINT + "?user=" + username)); URI loginUri = baseUri.resolve(URI.create(LOGIN_ENDPOINT + "?user=" + username));
@ -246,6 +264,8 @@ public class FroniusConfigAuthUtil {
Thread.sleep(500 * attemptCount); Thread.sleep(500 * attemptCount);
attemptCount++; attemptCount++;
lastException = e; lastException = e;
} catch (FroniusUnauthorizedException e) {
throw e;
} }
if (attemptCount >= 3) { 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}. * 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 static class XWwwAuthenticateHeaderListener extends Response.Listener.Adapter {
private final CountDownLatch latch; private final CountDownLatch latch;
@ -288,4 +311,30 @@ public class FroniusConfigAuthUtil {
return authHeader; 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.action.FroniusSymoInverterActions;
import org.openhab.binding.fronius.internal.api.FroniusBatteryControl; import org.openhab.binding.fronius.internal.api.FroniusBatteryControl;
import org.openhab.binding.fronius.internal.api.FroniusCommunicationException; 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.ValueUnit;
import org.openhab.binding.fronius.internal.api.dto.inverter.InverterDeviceStatus; import org.openhab.binding.fronius.internal.api.dto.inverter.InverterDeviceStatus;
import org.openhab.binding.fronius.internal.api.dto.inverter.InverterRealtimeBody; import org.openhab.binding.fronius.internal.api.dto.inverter.InverterRealtimeBody;
@ -115,6 +116,8 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
return true; return true;
} catch (FroniusCommunicationException e) { } catch (FroniusCommunicationException e) {
logger.warn("Failed to reset battery control", 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; return false;
@ -128,6 +131,8 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
return true; return true;
} catch (FroniusCommunicationException e) { } catch (FroniusCommunicationException e) {
logger.warn("Failed to set battery control to hold battery charge", 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; return false;
@ -141,6 +146,9 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
return true; return true;
} catch (FroniusCommunicationException e) { } catch (FroniusCommunicationException e) {
logger.warn("Failed to add hold battery charge schedule to battery control", 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; return false;
@ -154,6 +162,8 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
return true; return true;
} catch (FroniusCommunicationException e) { } catch (FroniusCommunicationException e) {
logger.warn("Failed to set battery control to force battery charge", 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; return false;
@ -167,6 +177,9 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
return true; return true;
} catch (FroniusCommunicationException e) { } catch (FroniusCommunicationException e) {
logger.warn("Failed to add forced battery charge schedule to battery control", 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; return false;